Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41cd6feff2 | ||
|
|
2443a96d1e | ||
|
|
3962f338a5 | ||
|
|
67e48331a1 | ||
|
|
a9bfba9d9e | ||
|
|
26dd65d7a3 | ||
|
|
b1093af893 | ||
|
|
b19ad9ccad | ||
|
|
4aae445e4a | ||
|
|
58f1e2c585 | ||
|
|
89901de4bd | ||
|
|
2e18d5023e | ||
|
|
d501f1fec6 | ||
|
|
2217da078a | ||
|
|
fadbf80093 | ||
|
|
9cb1ad8024 | ||
|
|
14b27552db | ||
|
|
1c940a0d25 | ||
|
|
1a7b2f1806 | ||
|
|
39aea6545d | ||
|
|
86c408c700 | ||
|
|
211cd05750 | ||
|
|
b7f0ec3199 | ||
|
|
f1825fb9fa | ||
|
|
719aba383b | ||
|
|
dc33b4b05c | ||
|
|
97a6e94d32 | ||
|
|
37a10c76ab | ||
|
|
3ebdac4df9 | ||
|
|
8d16834e39 | ||
|
|
d22c9e24f4 |
No files matched your search
Generated
+816
-660
File diff suppressed because it is too large.
Load diff
+36
-36
@@ -25,31 +25,31 @@ actix-ws = "0.3.0"
|
|||||||
argon2 = { version = "0.5.3", features = ["std"] }
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
ariadne = { path = "packages/ariadne" }
|
ariadne = { path = "packages/ariadne" }
|
||||||
async_zip = "0.0.17"
|
async_zip = "0.0.17"
|
||||||
async-compression = { version = "0.4.25", default-features = false }
|
async-compression = { version = "0.4.27", default-features = false }
|
||||||
async-recursion = "1.1.1"
|
async-recursion = "1.1.1"
|
||||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||||
"runtime-tokio-hyper-rustls",
|
"runtime-tokio-hyper-rustls",
|
||||||
] }
|
] }
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.88"
|
||||||
async-tungstenite = { version = "0.29.1", default-features = false, features = [
|
async-tungstenite = { version = "0.30.0", default-features = false, features = [
|
||||||
"futures-03-sink",
|
"futures-03-sink",
|
||||||
] }
|
] }
|
||||||
async-walkdir = "2.1.0"
|
async-walkdir = "2.1.0"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bitflags = "2.9.1"
|
bitflags = "2.9.1"
|
||||||
bytemuck = "1.23.0"
|
bytemuck = "1.23.1"
|
||||||
bytes = "1.10.1"
|
bytes = "1.10.1"
|
||||||
censor = "0.3.0"
|
censor = "0.3.0"
|
||||||
chardetng = "0.1.17"
|
chardetng = "0.1.17"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
clap = "4.5.40"
|
clap = "4.5.43"
|
||||||
clickhouse = "0.13.3"
|
clickhouse = "0.13.3"
|
||||||
color-thief = "0.2.2"
|
color-thief = "0.2.2"
|
||||||
console-subscriber = "0.4.1"
|
console-subscriber = "0.4.1"
|
||||||
daedalus = { path = "packages/daedalus" }
|
daedalus = { path = "packages/daedalus" }
|
||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
data-url = "0.3.1"
|
data-url = "0.3.1"
|
||||||
deadpool-redis = "0.21.1"
|
deadpool-redis = "0.22.0"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
discord-rich-presence = "0.2.5"
|
discord-rich-presence = "0.2.5"
|
||||||
dotenv-build = "0.1.1"
|
dotenv-build = "0.1.1"
|
||||||
@@ -57,7 +57,7 @@ dotenvy = "0.15.7"
|
|||||||
dunce = "1.0.5"
|
dunce = "1.0.5"
|
||||||
either = "1.15.0"
|
either = "1.15.0"
|
||||||
encoding_rs = "0.8.35"
|
encoding_rs = "0.8.35"
|
||||||
enumset = "1.1.6"
|
enumset = "1.1.7"
|
||||||
flate2 = "1.1.2"
|
flate2 = "1.1.2"
|
||||||
fs4 = { version = "0.13.1", default-features = false }
|
fs4 = { version = "0.13.1", default-features = false }
|
||||||
futures = { version = "0.3.31", default-features = false }
|
futures = { version = "0.3.31", default-features = false }
|
||||||
@@ -74,15 +74,15 @@ hyper-rustls = { version = "0.27.7", default-features = false, features = [
|
|||||||
"ring",
|
"ring",
|
||||||
"tls12",
|
"tls12",
|
||||||
] }
|
] }
|
||||||
hyper-util = "0.1.14"
|
hyper-util = "0.1.16"
|
||||||
iana-time-zone = "0.1.63"
|
iana-time-zone = "0.1.63"
|
||||||
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
||||||
indexmap = "2.9.0"
|
indexmap = "2.10.0"
|
||||||
indicatif = "0.17.11"
|
indicatif = "0.18.0"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
jemalloc_pprof = "0.7.0"
|
jemalloc_pprof = "0.8.1"
|
||||||
json-patch = { version = "4.0.0", default-features = false }
|
json-patch = { version = "4.0.0", default-features = false }
|
||||||
lettre = { version = "0.11.17", default-features = false, features = [
|
lettre = { version = "0.11.18", default-features = false, features = [
|
||||||
"builder",
|
"builder",
|
||||||
"hostname",
|
"hostname",
|
||||||
"pool",
|
"pool",
|
||||||
@@ -92,24 +92,24 @@ lettre = { version = "0.11.17", default-features = false, features = [
|
|||||||
"smtp-transport",
|
"smtp-transport",
|
||||||
] }
|
] }
|
||||||
maxminddb = "0.26.0"
|
maxminddb = "0.26.0"
|
||||||
meilisearch-sdk = { version = "0.28.0", default-features = false }
|
meilisearch-sdk = { version = "0.29.1", default-features = false }
|
||||||
murmur2 = "0.1.0"
|
murmur2 = "0.1.0"
|
||||||
native-dialog = "0.9.0"
|
native-dialog = "0.9.0"
|
||||||
notify = { version = "8.0.0", default-features = false }
|
notify = { version = "8.2.0", default-features = false }
|
||||||
notify-debouncer-mini = { version = "0.6.0", default-features = false }
|
notify-debouncer-mini = { version = "0.7.0", default-features = false }
|
||||||
p256 = "0.13.2"
|
p256 = "0.13.2"
|
||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
phf = { version = "0.12.1", features = ["macros"] }
|
phf = { version = "0.12.1", features = ["macros"] }
|
||||||
png = "0.17.16"
|
png = "0.17.16"
|
||||||
prometheus = "0.14.0"
|
prometheus = "0.14.0"
|
||||||
quartz_nbt = "0.2.9"
|
quartz_nbt = "0.2.9"
|
||||||
quick-xml = "0.37.5"
|
quick-xml = "0.38.1"
|
||||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||||
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
redis = "0.32.4"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
reqwest = { version = "0.12.20", default-features = false }
|
reqwest = { version = "0.12.22", default-features = false }
|
||||||
rgb = "0.8.50"
|
rgb = "0.8.52"
|
||||||
rust_decimal = { version = "1.37.2", features = [
|
rust_decimal = { version = "1.37.2", features = [
|
||||||
"serde-with-float",
|
"serde-with-float",
|
||||||
"serde-with-str",
|
"serde-with-str",
|
||||||
@@ -121,7 +121,7 @@ rust-s3 = { version = "0.35.1", default-features = false, features = [
|
|||||||
"tokio-rustls-tls",
|
"tokio-rustls-tls",
|
||||||
] }
|
] }
|
||||||
rusty-money = "0.4.1"
|
rusty-money = "0.4.1"
|
||||||
sentry = { version = "0.41.0", default-features = false, features = [
|
sentry = { version = "0.42.0", default-features = false, features = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"contexts",
|
"contexts",
|
||||||
"debug-images",
|
"debug-images",
|
||||||
@@ -129,45 +129,45 @@ sentry = { version = "0.41.0", default-features = false, features = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"rustls",
|
"rustls",
|
||||||
] }
|
] }
|
||||||
sentry-actix = "0.41.0"
|
sentry-actix = "0.42.0"
|
||||||
serde = "1.0.219"
|
serde = "1.0.219"
|
||||||
serde_bytes = "0.11.17"
|
serde_bytes = "0.11.17"
|
||||||
serde_cbor = "0.11.2"
|
serde_cbor = "0.11.2"
|
||||||
serde_ini = "0.2.0"
|
serde_ini = "0.2.0"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.142"
|
||||||
serde_with = "3.13.0"
|
serde_with = "3.14.0"
|
||||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||||
sha1 = "0.10.6"
|
sha1 = "0.10.6"
|
||||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
spdx = "0.10.8"
|
spdx = "0.10.9"
|
||||||
sqlx = { version = "0.8.6", default-features = false }
|
sqlx = { version = "0.8.6", default-features = false }
|
||||||
sysinfo = { version = "0.35.2", default-features = false }
|
sysinfo = { version = "0.36.1", default-features = false }
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
tauri = "2.6.1"
|
tauri = "2.7.0"
|
||||||
tauri-build = "2.3.0"
|
tauri-build = "2.3.1"
|
||||||
tauri-plugin-deep-link = "2.4.0"
|
tauri-plugin-deep-link = "2.4.1"
|
||||||
tauri-plugin-dialog = "2.3.0"
|
tauri-plugin-dialog = "2.3.2"
|
||||||
tauri-plugin-http = "2.5.0"
|
tauri-plugin-http = "2.5.1"
|
||||||
tauri-plugin-opener = "2.4.0"
|
tauri-plugin-opener = "2.4.0"
|
||||||
tauri-plugin-os = "2.3.0"
|
tauri-plugin-os = "2.3.0"
|
||||||
tauri-plugin-single-instance = "2.3.0"
|
tauri-plugin-single-instance = "2.3.2"
|
||||||
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
|
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"zip",
|
"zip",
|
||||||
] }
|
] }
|
||||||
tauri-plugin-window-state = "2.3.0"
|
tauri-plugin-window-state = "2.4.0"
|
||||||
tempfile = "3.20.0"
|
tempfile = "3.20.0"
|
||||||
theseus = { path = "packages/app-lib" }
|
theseus = { path = "packages/app-lib" }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tikv-jemalloc-ctl = "0.6.0"
|
tikv-jemalloc-ctl = "0.6.0"
|
||||||
tikv-jemallocator = "0.6.0"
|
tikv-jemallocator = "0.6.0"
|
||||||
tokio = "1.45.1"
|
tokio = "1.47.1"
|
||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
tokio-util = "0.7.15"
|
tokio-util = "0.7.16"
|
||||||
totp-rs = "5.7.0"
|
totp-rs = "5.7.0"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-actix-web = "0.7.18"
|
tracing-actix-web = "0.7.19"
|
||||||
tracing-error = "0.2.1"
|
tracing-error = "0.2.1"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
@@ -179,7 +179,7 @@ whoami = "1.6.0"
|
|||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
woothee = "0.13.0"
|
woothee = "0.13.0"
|
||||||
yaserde = "0.12.0"
|
yaserde = "0.12.0"
|
||||||
zip = { version = "4.2.0", default-features = false, features = [
|
zip = { version = "4.3.0", default-features = false, features = [
|
||||||
"bzip2",
|
"bzip2",
|
||||||
"deflate",
|
"deflate",
|
||||||
"deflate64",
|
"deflate64",
|
||||||
@@ -226,7 +226,7 @@ wildcard_dependencies = "warn"
|
|||||||
warnings = "deny"
|
warnings = "deny"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
wry = { git = "https://github.com/modrinth/wry", rev = "21db186" }
|
wry = { git = "https://github.com/modrinth/wry", rev = "f2ce0b0" }
|
||||||
|
|
||||||
# Optimize for speed and reduce size on release builds
|
# Optimize for speed and reduce size on release builds
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -197,16 +197,14 @@ pub async fn open_link<R: Runtime>(
|
|||||||
|
|
||||||
if url::Url::parse(&path).is_ok()
|
if url::Url::parse(&path).is_ok()
|
||||||
&& !state.malicious_origins.contains(&origin)
|
&& !state.malicious_origins.contains(&origin)
|
||||||
|
&& let Some(last_click) = state.last_click
|
||||||
|
&& last_click.elapsed() < Duration::from_millis(100)
|
||||||
{
|
{
|
||||||
if let Some(last_click) = state.last_click {
|
|
||||||
if last_click.elapsed() < Duration::from_millis(100) {
|
|
||||||
let _ = app.opener().open_url(&path, None::<String>);
|
let _ = app.opener().open_url(&path, None::<String>);
|
||||||
state.last_click = None;
|
state.last_click = None;
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!("Malicious click: {path} origin {origin}");
|
tracing::info!("Malicious click: {path} origin {origin}");
|
||||||
state.malicious_origins.insert(origin);
|
state.malicious_origins.insert(origin);
|
||||||
|
|||||||
@@ -59,17 +59,14 @@ pub async fn login<R: Runtime>(
|
|||||||
.url()?
|
.url()?
|
||||||
.as_str()
|
.as_str()
|
||||||
.starts_with("https://login.live.com/oauth20_desktop.srf")
|
.starts_with("https://login.live.com/oauth20_desktop.srf")
|
||||||
{
|
&& let Some((_, code)) =
|
||||||
if let Some((_, code)) =
|
|
||||||
window.url()?.query_pairs().find(|x| x.0 == "code")
|
window.url()?.query_pairs().find(|x| x.0 == "code")
|
||||||
{
|
{
|
||||||
window.close()?;
|
window.close()?;
|
||||||
let val =
|
let val = minecraft_auth::finish_login(&code.clone(), flow).await?;
|
||||||
minecraft_auth::finish_login(&code.clone(), flow).await?;
|
|
||||||
|
|
||||||
return Ok(Some(val));
|
return Ok(Some(val));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,12 +63,12 @@ pub async fn should_disable_mouseover() -> bool {
|
|||||||
// We try to match version to 12.2 or higher. If unrecognizable to pattern or lower, we default to the css with disabled mouseover for safety
|
// We try to match version to 12.2 or higher. If unrecognizable to pattern or lower, we default to the css with disabled mouseover for safety
|
||||||
if let tauri_plugin_os::Version::Semantic(major, minor, _) =
|
if let tauri_plugin_os::Version::Semantic(major, minor, _) =
|
||||||
tauri_plugin_os::version()
|
tauri_plugin_os::version()
|
||||||
|
&& major >= 12
|
||||||
|
&& minor >= 3
|
||||||
{
|
{
|
||||||
if major >= 12 && minor >= 3 {
|
|
||||||
// Mac os version is 12.3 or higher, we allow mouseover
|
// Mac os version is 12.3 or higher, we allow mouseover
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
// Not macos, we allow mouseover
|
// Not macos, we allow mouseover
|
||||||
|
|||||||
@@ -233,11 +233,11 @@ fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
if let Some(window) = app.get_window("main") {
|
if let Some(window) = app.get_window("main")
|
||||||
if let Err(e) = window.set_shadow(true) {
|
&& let Err(e) = window.set_shadow(true)
|
||||||
|
{
|
||||||
tracing::warn!("Failed to set window shadow: {e}");
|
tracing::warn!("Failed to set window shadow: {e}");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -506,17 +506,16 @@ async fn fetch(
|
|||||||
|
|
||||||
return Ok(lib);
|
return Ok(lib);
|
||||||
}
|
}
|
||||||
} else if let Some(url) = &lib.url {
|
} else if let Some(url) = &lib.url
|
||||||
if !url.is_empty() {
|
&& !url.is_empty()
|
||||||
|
{
|
||||||
insert_mirrored_artifact(
|
insert_mirrored_artifact(
|
||||||
&lib.name,
|
&lib.name,
|
||||||
None,
|
None,
|
||||||
vec![
|
vec![
|
||||||
url.clone(),
|
url.clone(),
|
||||||
"https://libraries.minecraft.net/"
|
"https://libraries.minecraft.net/".to_string(),
|
||||||
.to_string(),
|
"https://maven.creeperhost.net/".to_string(),
|
||||||
"https://maven.creeperhost.net/"
|
|
||||||
.to_string(),
|
|
||||||
maven_url.to_string(),
|
maven_url.to_string(),
|
||||||
],
|
],
|
||||||
false,
|
false,
|
||||||
@@ -527,7 +526,6 @@ async fn fetch(
|
|||||||
|
|
||||||
return Ok(lib);
|
return Ok(lib);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Other libraries are generally available in the "maven" directory of the installer. If they are
|
// Other libraries are generally available in the "maven" directory of the installer. If they are
|
||||||
// not present here, they will be generated by Forge processors.
|
// not present here, they will be generated by Forge processors.
|
||||||
|
|||||||
@@ -93,9 +93,9 @@ async fn main() -> Result<()> {
|
|||||||
.ok()
|
.ok()
|
||||||
.and_then(|x| x.parse::<bool>().ok())
|
.and_then(|x| x.parse::<bool>().ok())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
|
&& let Ok(token) = dotenvy::var("CLOUDFLARE_TOKEN")
|
||||||
|
&& let Ok(zone_id) = dotenvy::var("CLOUDFLARE_ZONE_ID")
|
||||||
{
|
{
|
||||||
if let Ok(token) = dotenvy::var("CLOUDFLARE_TOKEN") {
|
|
||||||
if let Ok(zone_id) = dotenvy::var("CLOUDFLARE_ZONE_ID") {
|
|
||||||
let cache_clears = upload_files
|
let cache_clears = upload_files
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| format_url(&x.0))
|
.map(|x| format_url(&x.0))
|
||||||
@@ -130,8 +130,6 @@ async fn main() -> Result<()> {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,20 +167,18 @@ pub async fn download_file(
|
|||||||
let bytes = x.bytes().await;
|
let bytes = x.bytes().await;
|
||||||
|
|
||||||
if let Ok(bytes) = bytes {
|
if let Ok(bytes) = bytes {
|
||||||
if let Some(sha1) = sha1 {
|
if let Some(sha1) = sha1
|
||||||
if &*sha1_async(bytes.clone()).await? != sha1 {
|
&& &*sha1_async(bytes.clone()).await? != sha1
|
||||||
|
{
|
||||||
if attempt <= 3 {
|
if attempt <= 3 {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
return Err(
|
return Err(crate::ErrorKind::ChecksumFailure {
|
||||||
crate::ErrorKind::ChecksumFailure {
|
|
||||||
hash: sha1.to_string(),
|
hash: sha1.to_string(),
|
||||||
url: url.to_string(),
|
url: url.to_string(),
|
||||||
tries: attempt,
|
tries: attempt,
|
||||||
}
|
}
|
||||||
.into(),
|
.into());
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,8 +143,13 @@ export default defineNuxtConfig({
|
|||||||
state.lastGenerated &&
|
state.lastGenerated &&
|
||||||
new Date(state.lastGenerated).getTime() + TTL > new Date().getTime() &&
|
new Date(state.lastGenerated).getTime() + TTL > new Date().getTime() &&
|
||||||
// ...but only if the API URL is the same
|
// ...but only if the API URL is the same
|
||||||
state.apiUrl === API_URL
|
state.apiUrl === API_URL &&
|
||||||
|
// ...and if no errors were caught during the last generation
|
||||||
|
(state.errors ?? []).length === 0
|
||||||
) {
|
) {
|
||||||
|
console.log(
|
||||||
|
"Tags already recently generated. Delete apps/frontend/generated/state.json to force regeneration.",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,481 +1,510 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="showInvitation" class="universal-card information invited my-4">
|
<div v-if="showInvitation" class="universal-card information invited">
|
||||||
<h2>{{ getFormattedMessage(messages.invitationTitle) }}</h2>
|
<h2>Invitation to join project</h2>
|
||||||
<p v-if="currentMember?.project_role">
|
<p>
|
||||||
{{ formatMessage(messages.invitationWithRole, { role: currentMember.project_role }) }}
|
You've been invited be a member of this project with the role of '{{ currentMember.role }}'.
|
||||||
</p>
|
</p>
|
||||||
<p v-else>{{ getFormattedMessage(messages.invitationNoRole) }}</p>
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<ButtonStyled color="brand">
|
<button class="iconified-button brand-button" @click="acceptInvite()">
|
||||||
<button class="brand-button" @click="acceptInvite()">
|
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
{{ getFormattedMessage(messages.accept) }}
|
Accept
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
<button class="iconified-button danger-button" @click="declineInvite()">
|
||||||
<ButtonStyled color="red">
|
|
||||||
<button @click="declineInvite">
|
|
||||||
<XIcon />
|
<XIcon />
|
||||||
{{ getFormattedMessage(messages.decline) }}
|
Decline
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
currentMember &&
|
currentMember &&
|
||||||
visibleNags.length > 0 &&
|
nags.filter((x) => x.condition).length > 0 &&
|
||||||
(project.status === 'draft' || tags.rejectedStatuses.includes(project.status))
|
(project.status === 'draft' || tags.rejectedStatuses.includes(project.status))
|
||||||
"
|
"
|
||||||
class="universal-card my-4"
|
class="author-actions universal-card mb-4"
|
||||||
>
|
>
|
||||||
<div class="flex max-w-full flex-wrap items-center gap-x-6 gap-y-4">
|
<div class="header__row">
|
||||||
<div class="flex flex-auto flex-wrap items-center gap-x-6 gap-y-4">
|
<div class="header__title">
|
||||||
<h2 class="my-0 mr-auto">{{ getFormattedMessage(messages.publishingChecklist) }}</h2>
|
<h2>Publishing checklist</h2>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="checklist">
|
||||||
<div class="flex items-center gap-1">
|
<span class="checklist__title">Progress:</span>
|
||||||
<AsteriskIcon class="size-4 text-red" />
|
<div class="checklist__items">
|
||||||
<span class="text-secondary">{{ getFormattedMessage(messages.required) }}</span>
|
<div
|
||||||
|
v-for="nag in nags"
|
||||||
|
:key="`checklist-${nag.id}`"
|
||||||
|
v-tooltip="nag.title"
|
||||||
|
:aria-label="nag.title"
|
||||||
|
:class="'circle ' + (!nag.condition ? 'done' : '') + nag.status"
|
||||||
|
class="circle"
|
||||||
|
>
|
||||||
|
<CheckIcon v-if="!nag.condition" />
|
||||||
|
<AsteriskIcon v-else-if="nag.status === 'required'" />
|
||||||
|
<LightBulbIcon v-else-if="nag.status === 'suggestion'" />
|
||||||
|
<ScaleIcon v-else-if="nag.status === 'review'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<TriangleAlertIcon class="size-4 text-orange" />
|
|
||||||
<span class="text-secondary">{{ getFormattedMessage(messages.warning) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<LightBulbIcon class="size-4 text-purple" />
|
|
||||||
<span class="text-secondary">{{ getFormattedMessage(messages.suggestion) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<ButtonStyled circular>
|
<button
|
||||||
<button :class="!collapsed && '[&>svg]:rotate-180'" @click="toggleCollapsed()">
|
:class="{ 'not-collapsed': !collapsed }"
|
||||||
<DropdownIcon class="duration-250 transition-transform ease-in-out" />
|
class="square-button"
|
||||||
|
@click="toggleCollapsed()"
|
||||||
|
>
|
||||||
|
<DropdownIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!collapsed" class="grid-display width-16 mt-4">
|
<div v-if="!collapsed" class="grid-display width-16">
|
||||||
<div v-for="nag in visibleNags" :key="nag.id" class="grid-display__item">
|
<div
|
||||||
<span class="flex items-center gap-2 font-semibold">
|
v-for="nag in nags.filter((x) => x.condition && !x.hide)"
|
||||||
<component
|
:key="nag.id"
|
||||||
:is="nag.icon || getDefaultIcon(nag.status)"
|
class="grid-display__item"
|
||||||
v-tooltip="getStatusTooltip(nag.status)"
|
>
|
||||||
:class="[
|
<span class="label">
|
||||||
'size-4',
|
<AsteriskIcon
|
||||||
nag.status === 'required' && 'text-red',
|
v-if="nag.status === 'required'"
|
||||||
nag.status === 'warning' && 'text-orange',
|
v-tooltip="'Required'"
|
||||||
nag.status === 'suggestion' && 'text-purple',
|
:class="nag.status"
|
||||||
]"
|
aria-label="Required"
|
||||||
:aria-label="getStatusTooltip(nag.status)"
|
|
||||||
/>
|
/>
|
||||||
{{ getFormattedMessage(nag.title) }}
|
<LightBulbIcon
|
||||||
</span>
|
v-else-if="nag.status === 'suggestion'"
|
||||||
{{ getNagDescription(nag) }}
|
v-tooltip="'Suggestion'"
|
||||||
|
:class="nag.status"
|
||||||
|
aria-label="Suggestion"
|
||||||
|
/>
|
||||||
|
<ScaleIcon
|
||||||
|
v-else-if="nag.status === 'review'"
|
||||||
|
v-tooltip="'Review'"
|
||||||
|
:class="nag.status"
|
||||||
|
aria-label="Review"
|
||||||
|
/>{{ nag.title }}</span
|
||||||
|
>
|
||||||
|
{{ nag.description }}
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="nag.link && shouldShowLink(nag)"
|
v-if="nag.link"
|
||||||
|
:class="{ invisible: nag.link.hide }"
|
||||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/${
|
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/${
|
||||||
nag.link.path
|
nag.link.path
|
||||||
}`"
|
}`"
|
||||||
class="goto-link"
|
class="goto-link"
|
||||||
>
|
>
|
||||||
{{ getFormattedMessage(nag.link.title) }}
|
{{ nag.link.title }}
|
||||||
<ChevronRightIcon aria-hidden="true" class="featured-header-chevron" />
|
<ChevronRightIcon aria-hidden="true" class="featured-header-chevron" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<ButtonStyled
|
|
||||||
v-if="nag.status === 'special-submit-action' && nag.id === 'submit-for-review'"
|
|
||||||
color="orange"
|
|
||||||
@click="submitForReview"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
:disabled="!canSubmitForReview"
|
v-else-if="nag.action"
|
||||||
v-tooltip="
|
:disabled="nag.action.disabled()"
|
||||||
!canSubmitForReview ? getFormattedMessage(messages.submitChecklistTooltip) : undefined
|
class="btn btn-orange"
|
||||||
"
|
@click="nag.action.onClick"
|
||||||
>
|
>
|
||||||
<SendIcon />
|
<SendIcon />
|
||||||
{{ getFormattedMessage(messages.submitForReview) }}
|
{{ nag.action.title }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup>
|
||||||
import {
|
import {
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
AsteriskIcon,
|
AsteriskIcon,
|
||||||
LightBulbIcon,
|
LightBulbIcon,
|
||||||
TriangleAlertIcon,
|
|
||||||
DropdownIcon,
|
|
||||||
SendIcon,
|
SendIcon,
|
||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
InfoIcon,
|
DropdownIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
|
import { formatProjectType } from "@modrinth/utils";
|
||||||
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
||||||
import { nags } from "@modrinth/moderation";
|
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
|
||||||
import { useVIntl, defineMessages, type MessageDescriptor } from "@vintl/vintl";
|
|
||||||
import type { Nag, NagContext, NagStatus } from "@modrinth/moderation";
|
|
||||||
import type { Project, User, Version } from "@modrinth/utils";
|
|
||||||
import type { Component } from "vue";
|
|
||||||
|
|
||||||
interface Tags {
|
const props = defineProps({
|
||||||
rejectedStatuses: string[];
|
project: {
|
||||||
}
|
type: Object,
|
||||||
|
required: true,
|
||||||
interface Auth {
|
},
|
||||||
user: {
|
versions: {
|
||||||
id: string;
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentMember: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
allMembers: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
isSettings: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
collapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
routeName: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
setProcessing: {
|
||||||
|
type: Function,
|
||||||
|
default() {
|
||||||
|
return () => {
|
||||||
|
addNotification({
|
||||||
|
group: "main",
|
||||||
|
title: "An error occurred",
|
||||||
|
text: "setProcessing function not found",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
interface Member {
|
|
||||||
accepted?: boolean;
|
|
||||||
project_role?: string;
|
|
||||||
user?: Partial<User>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
project: Project;
|
|
||||||
versions?: Version[];
|
|
||||||
currentMember?: Member | null;
|
|
||||||
allMembers?: Member[] | null;
|
|
||||||
isSettings?: boolean;
|
|
||||||
collapsed?: boolean;
|
|
||||||
routeName?: string;
|
|
||||||
auth: Auth;
|
|
||||||
tags: Tags;
|
|
||||||
setProcessing?: (processing: boolean) => void;
|
|
||||||
toggleCollapsed?: () => void;
|
|
||||||
updateMembers?: () => void | Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
invitationTitle: {
|
|
||||||
id: "project-member-header.invitation-title",
|
|
||||||
defaultMessage: "Invitation to join project",
|
|
||||||
},
|
},
|
||||||
invitationWithRole: {
|
|
||||||
id: "project-member-header.invitation-with-role",
|
|
||||||
defaultMessage: "You've been invited be a member of this project with the role of '{role}'.",
|
|
||||||
},
|
},
|
||||||
invitationNoRole: {
|
toggleCollapsed: {
|
||||||
id: "project-member-header.invitation-no-role",
|
type: Function,
|
||||||
defaultMessage:
|
default() {
|
||||||
"You've been invited to join this project. Please accept or decline the invitation.",
|
return () => {
|
||||||
|
addNotification({
|
||||||
|
group: "main",
|
||||||
|
title: "An error occurred",
|
||||||
|
text: "toggleCollapsed function not found",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
};
|
||||||
},
|
},
|
||||||
accept: {
|
|
||||||
id: "project-member-header.accept",
|
|
||||||
defaultMessage: "Accept",
|
|
||||||
},
|
},
|
||||||
decline: {
|
updateMembers: {
|
||||||
id: "project-member-header.decline",
|
type: Function,
|
||||||
defaultMessage: "Decline",
|
default() {
|
||||||
|
return () => {
|
||||||
|
addNotification({
|
||||||
|
group: "main",
|
||||||
|
title: "An error occurred",
|
||||||
|
text: "updateMembers function not found",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
};
|
||||||
},
|
},
|
||||||
publishingChecklist: {
|
|
||||||
id: "project-member-header.publishing-checklist",
|
|
||||||
defaultMessage: "Publishing checklist",
|
|
||||||
},
|
|
||||||
submitForReview: {
|
|
||||||
id: "project-member-header.submit-for-review",
|
|
||||||
defaultMessage: "Submit for review",
|
|
||||||
},
|
|
||||||
submitForReviewDesc: {
|
|
||||||
id: "project-member-header.submit-for-review-desc",
|
|
||||||
defaultMessage:
|
|
||||||
"Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.",
|
|
||||||
},
|
|
||||||
resubmitForReview: {
|
|
||||||
id: "project-member-header.resubmit-for-review",
|
|
||||||
defaultMessage: "Resubmit for review",
|
|
||||||
},
|
|
||||||
resubmitForReviewDesc: {
|
|
||||||
id: "project-member-header.resubmit-for-review-desc",
|
|
||||||
defaultMessage:
|
|
||||||
"Your project has been {status} by Modrinth's staff. In most cases, you can resubmit for review after addressing the staff's message.",
|
|
||||||
},
|
|
||||||
showKey: {
|
|
||||||
id: "project-member-header.show-key",
|
|
||||||
defaultMessage: "Toggle key",
|
|
||||||
},
|
|
||||||
keyTitle: {
|
|
||||||
id: "project-member-header.key-title",
|
|
||||||
defaultMessage: "Status Key",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
id: "project-member-header.action",
|
|
||||||
defaultMessage: "Action",
|
|
||||||
},
|
|
||||||
visitModerationPage: {
|
|
||||||
id: "project-member-header.visit-moderation-page",
|
|
||||||
defaultMessage: "Visit moderation page",
|
|
||||||
},
|
|
||||||
submitChecklistTooltip: {
|
|
||||||
id: "project-member-header.submit-checklist-tooltip",
|
|
||||||
defaultMessage: "You must complete the required steps in the publishing checklist!",
|
|
||||||
},
|
|
||||||
successJoin: {
|
|
||||||
id: "project-member-header.success-join",
|
|
||||||
defaultMessage: "You have joined the project team",
|
|
||||||
},
|
|
||||||
errorJoin: {
|
|
||||||
id: "project-member-header.error-join",
|
|
||||||
defaultMessage: "Failed to accept team invitation",
|
|
||||||
},
|
|
||||||
successDecline: {
|
|
||||||
id: "project-member-header.success-decline",
|
|
||||||
defaultMessage: "You have declined the team invitation",
|
|
||||||
},
|
|
||||||
errorDecline: {
|
|
||||||
id: "project-member-header.error-decline",
|
|
||||||
defaultMessage: "Failed to decline team invitation",
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
id: "project-member-header.success",
|
|
||||||
defaultMessage: "Success",
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
id: "project-member-header.error",
|
|
||||||
defaultMessage: "Error",
|
|
||||||
},
|
|
||||||
required: {
|
|
||||||
id: "project-member-header.required",
|
|
||||||
defaultMessage: "Required",
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
id: "project-member-header.warning",
|
|
||||||
defaultMessage: "Warning",
|
|
||||||
},
|
|
||||||
suggestion: {
|
|
||||||
id: "project-member-header.suggestion",
|
|
||||||
defaultMessage: "Suggestion",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
const featuredGalleryImage = computed(() => props.project.gallery.find((img) => img.featured));
|
||||||
|
|
||||||
function getNagDescription(nag: Nag): string {
|
const nags = computed(() => [
|
||||||
if (typeof nag.description === "function") {
|
{
|
||||||
return nag.description(nagContext.value);
|
condition: props.versions.length < 1,
|
||||||
}
|
title: "Upload a version",
|
||||||
return formatMessage(nag.description);
|
id: "upload-version",
|
||||||
}
|
description: "At least one version is required for a project to be submitted for review.",
|
||||||
|
status: "required",
|
||||||
function getFormattedMessage(message: string | MessageDescriptor): string {
|
link: {
|
||||||
if (typeof message === "string") {
|
path: "versions",
|
||||||
return message;
|
title: "Visit versions page",
|
||||||
}
|
hide: props.routeName === "type-id-versions",
|
||||||
return formatMessage(message);
|
},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
condition:
|
||||||
versions: () => [],
|
props.project.body === "" || props.project.body.startsWith("# Placeholder description"),
|
||||||
currentMember: null,
|
title: "Add a description",
|
||||||
allMembers: null,
|
id: "add-description",
|
||||||
isSettings: false,
|
description:
|
||||||
collapsed: false,
|
"A description that clearly describes the project's purpose and function is required.",
|
||||||
routeName: "",
|
status: "required",
|
||||||
});
|
link: {
|
||||||
|
path: "settings/description",
|
||||||
const emit = defineEmits<{
|
title: "Visit description settings",
|
||||||
toggleCollapsed: [];
|
hide: props.routeName === "type-id-settings-description",
|
||||||
updateMembers: [];
|
},
|
||||||
setProcessing: [processing: boolean];
|
},
|
||||||
}>();
|
{
|
||||||
|
condition: !props.project.icon_url,
|
||||||
const nagContext = computed<NagContext>(() => ({
|
title: "Add an icon",
|
||||||
project: props.project,
|
id: "add-icon",
|
||||||
versions: props.versions,
|
description:
|
||||||
currentMember: props.currentMember as User,
|
"Your project should have a nice-looking icon to uniquely identify your project at a glance.",
|
||||||
currentRoute: props.routeName,
|
status: "suggestion",
|
||||||
tags: props.tags,
|
link: {
|
||||||
submitProject: submitForReview,
|
path: "settings",
|
||||||
}));
|
title: "Visit general settings",
|
||||||
|
hide: props.routeName === "type-id-settings",
|
||||||
const showKey = ref(false);
|
},
|
||||||
function toggleKey(): void {
|
},
|
||||||
showKey.value = !showKey.value;
|
{
|
||||||
}
|
condition: props.project.gallery.length === 0 || !featuredGalleryImage,
|
||||||
|
title: "Feature a gallery image",
|
||||||
const canSubmitForReview = computed(() => {
|
id: "feature-gallery-image",
|
||||||
return (
|
description: "Featured gallery images may be the first impression of many users.",
|
||||||
applicableNags.value.filter((nag) => nag.status === "required" && !isNagComplete(nag))
|
status: "suggestion",
|
||||||
.length === 0
|
link: {
|
||||||
);
|
path: "gallery",
|
||||||
});
|
title: "Visit gallery page",
|
||||||
|
hide: props.routeName === "type-id-gallery",
|
||||||
async function submitForReview() {
|
},
|
||||||
if (canSubmitForReview) {
|
},
|
||||||
await setProcessing(true);
|
{
|
||||||
}
|
hide: props.project.versions.length === 0,
|
||||||
}
|
condition: props.project.categories.length < 1,
|
||||||
|
title: "Select tags",
|
||||||
const applicableNags = computed<Nag[]>(() => {
|
id: "select-tags",
|
||||||
return nags.filter((nag) => {
|
description: "Select all tags that apply to your project.",
|
||||||
return nag.shouldShow(nagContext.value);
|
status: "suggestion",
|
||||||
});
|
link: {
|
||||||
});
|
path: "settings/tags",
|
||||||
|
title: "Visit tag settings",
|
||||||
function isNagComplete(nag: Nag): boolean {
|
hide: props.routeName === "type-id-settings-tags",
|
||||||
const context = nagContext.value;
|
},
|
||||||
return !nag.shouldShow(context);
|
},
|
||||||
}
|
{
|
||||||
|
condition: !(
|
||||||
const visibleNags = computed<Nag[]>(() => {
|
props.project.issues_url ||
|
||||||
const finalNags = applicableNags.value.filter((nag) => !isNagComplete(nag));
|
props.project.source_url ||
|
||||||
|
props.project.wiki_url ||
|
||||||
if (props.project.status === "draft") {
|
props.project.discord_url ||
|
||||||
finalNags.push({
|
props.project.donation_urls.length > 0
|
||||||
|
),
|
||||||
|
title: "Add external links",
|
||||||
|
id: "add-links",
|
||||||
|
description:
|
||||||
|
"Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.",
|
||||||
|
status: "suggestion",
|
||||||
|
link: {
|
||||||
|
path: "settings/links",
|
||||||
|
title: "Visit links settings",
|
||||||
|
hide: props.routeName === "type-id-settings-links",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hide:
|
||||||
|
props.project.versions.length === 0 ||
|
||||||
|
props.project.project_type === "resourcepack" ||
|
||||||
|
props.project.project_type === "plugin" ||
|
||||||
|
props.project.project_type === "shader" ||
|
||||||
|
props.project.project_type === "datapack",
|
||||||
|
condition:
|
||||||
|
props.project.client_side === "unknown" ||
|
||||||
|
props.project.server_side === "unknown" ||
|
||||||
|
(props.project.client_side === "unsupported" && props.project.server_side === "unsupported"),
|
||||||
|
title: "Select supported environments",
|
||||||
|
id: "select-environments",
|
||||||
|
description: `Select if the ${formatProjectType(
|
||||||
|
props.project.project_type,
|
||||||
|
).toLowerCase()} functions on the client-side and/or server-side.`,
|
||||||
|
status: "required",
|
||||||
|
link: {
|
||||||
|
path: "settings",
|
||||||
|
title: "Visit general settings",
|
||||||
|
hide: props.routeName === "type-id-settings",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: props.project.license.id === "LicenseRef-Unknown",
|
||||||
|
title: "Select license",
|
||||||
|
id: "select-license",
|
||||||
|
description: `Select the license your ${formatProjectType(
|
||||||
|
props.project.project_type,
|
||||||
|
).toLowerCase()} is distributed under.`,
|
||||||
|
status: "required",
|
||||||
|
link: {
|
||||||
|
path: "settings/license",
|
||||||
|
title: "Visit license settings",
|
||||||
|
hide: props.routeName === "type-id-settings-license",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: props.project.status === "draft",
|
||||||
|
title: "Submit for review",
|
||||||
id: "submit-for-review",
|
id: "submit-for-review",
|
||||||
title: messages.submitForReview,
|
description:
|
||||||
description: () => formatMessage(messages.submitForReviewDesc),
|
"Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.",
|
||||||
status: "special-submit-action",
|
status: "review",
|
||||||
shouldShow: (ctx) => ctx.project.status === "draft",
|
link: null,
|
||||||
});
|
action: {
|
||||||
}
|
onClick: submitForReview,
|
||||||
|
title: "Submit for review",
|
||||||
if (props.tags.rejectedStatuses.includes(props.project.status)) {
|
disabled: () => nags.value.filter((x) => x.condition && x.status === "required").length > 0,
|
||||||
finalNags.push({
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hide: props.project.stats === "draft",
|
||||||
|
condition: props.tags.rejectedStatuses.includes(props.project.status),
|
||||||
|
title: "Resubmit for review",
|
||||||
id: "resubmit-for-review",
|
id: "resubmit-for-review",
|
||||||
title: messages.resubmitForReview,
|
description: `Your project has been ${props.project.status} by
|
||||||
description: (ctx) =>
|
Modrinth's staff. In most cases, you can resubmit for review after
|
||||||
formatMessage(messages.resubmitForReviewDesc, { status: ctx.project.status }),
|
addressing the staff's message.`,
|
||||||
status: "special-submit-action",
|
status: "review",
|
||||||
shouldShow: (ctx) => ctx.tags.rejectedStatuses.includes(ctx.project.status),
|
|
||||||
link: {
|
link: {
|
||||||
path: "moderation",
|
path: "moderation",
|
||||||
title: messages.visitModerationPage,
|
title: "Visit moderation page",
|
||||||
shouldShow: () => props.routeName !== "type-id-moderation",
|
hide: props.routeName === "type-id-moderation",
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
]);
|
||||||
|
|
||||||
finalNags.sort((a, b) => {
|
const showInvitation = computed(() => {
|
||||||
const statusOrder = { required: 0, warning: 1, suggestion: 2, "special-submit-action": 3 };
|
|
||||||
return statusOrder[a.status] - statusOrder[b.status];
|
|
||||||
});
|
|
||||||
|
|
||||||
return finalNags;
|
|
||||||
});
|
|
||||||
|
|
||||||
function shouldShowLink(nag: Nag): boolean {
|
|
||||||
return nag.link?.shouldShow ? nag.link.shouldShow(nagContext.value) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultIcon(status: NagStatus): Component {
|
|
||||||
switch (status) {
|
|
||||||
case "required":
|
|
||||||
return AsteriskIcon;
|
|
||||||
case "warning":
|
|
||||||
return TriangleAlertIcon;
|
|
||||||
case "suggestion":
|
|
||||||
return LightBulbIcon;
|
|
||||||
case "special-submit-action":
|
|
||||||
return ScaleIcon;
|
|
||||||
default:
|
|
||||||
return AsteriskIcon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusTooltip(status: NagStatus): string {
|
|
||||||
switch (status) {
|
|
||||||
case "required":
|
|
||||||
return formatMessage(messages.required);
|
|
||||||
case "warning":
|
|
||||||
return formatMessage(messages.warning);
|
|
||||||
case "suggestion":
|
|
||||||
return formatMessage(messages.suggestion);
|
|
||||||
default:
|
|
||||||
return formatMessage(messages.required);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const showInvitation = computed<boolean>(() => {
|
|
||||||
if (props.allMembers && props.auth) {
|
if (props.allMembers && props.auth) {
|
||||||
const member = props.allMembers.find((x) => x?.user?.id === props.auth.user.id);
|
const member = props.allMembers.find((x) => x.user.id === props.auth.user.id);
|
||||||
return !!member && !member.accepted;
|
return member && !member.accepted;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
function toggleCollapsed(): void {
|
const acceptInvite = () => {
|
||||||
if (props.toggleCollapsed) {
|
acceptTeamInvite(props.project.team);
|
||||||
props.toggleCollapsed();
|
props.updateMembers();
|
||||||
} else {
|
};
|
||||||
emit("toggleCollapsed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateMembers(): Promise<void> {
|
const declineInvite = () => {
|
||||||
if (props.updateMembers) {
|
removeTeamMember(props.project.team, props.auth.user.id);
|
||||||
await props.updateMembers();
|
props.updateMembers();
|
||||||
} else {
|
};
|
||||||
emit("updateMembers");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setProcessing(processing: boolean): void {
|
const submitForReview = async () => {
|
||||||
if (props.setProcessing) {
|
if (
|
||||||
props.setProcessing(processing);
|
!props.acknowledgedMessage ||
|
||||||
} else {
|
nags.value.filter((x) => x.condition && x.status === "required").length === 0
|
||||||
emit("setProcessing", processing);
|
) {
|
||||||
}
|
await props.setProcessing();
|
||||||
}
|
|
||||||
|
|
||||||
async function acceptInvite(): Promise<void> {
|
|
||||||
try {
|
|
||||||
setProcessing(true);
|
|
||||||
await acceptTeamInvite(props.project.team);
|
|
||||||
await updateMembers();
|
|
||||||
addNotification({
|
|
||||||
group: "main",
|
|
||||||
title: formatMessage(messages.success),
|
|
||||||
text: formatMessage(messages.successJoin),
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
addNotification({
|
|
||||||
group: "main",
|
|
||||||
title: formatMessage(messages.error),
|
|
||||||
text: formatMessage(messages.errorJoin),
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setProcessing(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function declineInvite(): Promise<void> {
|
|
||||||
try {
|
|
||||||
setProcessing(true);
|
|
||||||
await removeTeamMember(props.project.team, props.auth.user.id);
|
|
||||||
await updateMembers();
|
|
||||||
addNotification({
|
|
||||||
group: "main",
|
|
||||||
title: formatMessage(messages.success),
|
|
||||||
text: formatMessage(messages.successDecline),
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
addNotification({
|
|
||||||
group: "main",
|
|
||||||
title: formatMessage(messages.error),
|
|
||||||
text: formatMessage(messages.errorDecline),
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setProcessing(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.duration-250 {
|
.invited {
|
||||||
transition-duration: 250ms;
|
}
|
||||||
|
|
||||||
|
.author-actions {
|
||||||
|
margin-top: var(--spacing-card-md);
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__row {
|
||||||
|
align-items: center;
|
||||||
|
column-gap: var(--spacing-card-lg);
|
||||||
|
row-gap: var(--spacing-card-md);
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
.header__title {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: var(--spacing-card-lg);
|
||||||
|
row-gap: var(--spacing-card-md);
|
||||||
|
flex-basis: min-content;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 auto 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
svg {
|
||||||
|
transition: transform 0.25s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.not-collapsed svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-display__item .label {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-card-xs);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: var(--color-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion {
|
||||||
|
color: var(--color-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.review {
|
||||||
|
color: var(--color-orange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-card-xs);
|
||||||
|
width: fit-content;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
.checklist__title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: var(--spacing-card-xs);
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist__items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-card-xs);
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
--circle-size: 2rem;
|
||||||
|
--background-color: var(--color-bg);
|
||||||
|
--content-color: var(--color-gray);
|
||||||
|
width: var(--circle-size);
|
||||||
|
height: var(--circle-size);
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--content-color);
|
||||||
|
width: calc(var(--circle-size) / 2);
|
||||||
|
height: calc(var(--circle-size) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.required {
|
||||||
|
--content-color: var(--color-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.suggestion {
|
||||||
|
--content-color: var(--color-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.review {
|
||||||
|
--content-color: var(--color-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.done {
|
||||||
|
--background-color: var(--color-green);
|
||||||
|
--content-color: var(--color-brand-inverted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -554,78 +554,6 @@
|
|||||||
"profile.user-id": {
|
"profile.user-id": {
|
||||||
"message": "User ID: {id}"
|
"message": "User ID: {id}"
|
||||||
},
|
},
|
||||||
"project-member-header.accept": {
|
|
||||||
"message": "Accept"
|
|
||||||
},
|
|
||||||
"project-member-header.action": {
|
|
||||||
"message": "Action"
|
|
||||||
},
|
|
||||||
"project-member-header.decline": {
|
|
||||||
"message": "Decline"
|
|
||||||
},
|
|
||||||
"project-member-header.error": {
|
|
||||||
"message": "Error"
|
|
||||||
},
|
|
||||||
"project-member-header.error-decline": {
|
|
||||||
"message": "Failed to decline team invitation"
|
|
||||||
},
|
|
||||||
"project-member-header.error-join": {
|
|
||||||
"message": "Failed to accept team invitation"
|
|
||||||
},
|
|
||||||
"project-member-header.invitation-no-role": {
|
|
||||||
"message": "You've been invited to join this project. Please accept or decline the invitation."
|
|
||||||
},
|
|
||||||
"project-member-header.invitation-title": {
|
|
||||||
"message": "Invitation to join project"
|
|
||||||
},
|
|
||||||
"project-member-header.invitation-with-role": {
|
|
||||||
"message": "You've been invited be a member of this project with the role of '{role}'."
|
|
||||||
},
|
|
||||||
"project-member-header.key-title": {
|
|
||||||
"message": "Status Key"
|
|
||||||
},
|
|
||||||
"project-member-header.publishing-checklist": {
|
|
||||||
"message": "Publishing checklist"
|
|
||||||
},
|
|
||||||
"project-member-header.required": {
|
|
||||||
"message": "Required"
|
|
||||||
},
|
|
||||||
"project-member-header.resubmit-for-review": {
|
|
||||||
"message": "Resubmit for review"
|
|
||||||
},
|
|
||||||
"project-member-header.resubmit-for-review-desc": {
|
|
||||||
"message": "Your project has been {status} by Modrinth's staff. In most cases, you can resubmit for review after addressing the staff's message."
|
|
||||||
},
|
|
||||||
"project-member-header.show-key": {
|
|
||||||
"message": "Toggle key"
|
|
||||||
},
|
|
||||||
"project-member-header.submit-checklist-tooltip": {
|
|
||||||
"message": "You must complete the required steps in the publishing checklist!"
|
|
||||||
},
|
|
||||||
"project-member-header.submit-for-review": {
|
|
||||||
"message": "Submit for review"
|
|
||||||
},
|
|
||||||
"project-member-header.submit-for-review-desc": {
|
|
||||||
"message": "Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published."
|
|
||||||
},
|
|
||||||
"project-member-header.success": {
|
|
||||||
"message": "Success"
|
|
||||||
},
|
|
||||||
"project-member-header.success-decline": {
|
|
||||||
"message": "You have declined the team invitation"
|
|
||||||
},
|
|
||||||
"project-member-header.success-join": {
|
|
||||||
"message": "You have joined the project team"
|
|
||||||
},
|
|
||||||
"project-member-header.suggestion": {
|
|
||||||
"message": "Suggestion"
|
|
||||||
},
|
|
||||||
"project-member-header.visit-moderation-page": {
|
|
||||||
"message": "Visit moderation page"
|
|
||||||
},
|
|
||||||
"project-member-header.warning": {
|
|
||||||
"message": "Warning"
|
|
||||||
},
|
|
||||||
"project-type.collection.plural": {
|
"project-type.collection.plural": {
|
||||||
"message": "Collections"
|
"message": "Collections"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,10 +22,6 @@
|
|||||||
"
|
"
|
||||||
:on-image-upload="onUploadHandler"
|
:on-image-upload="onUploadHandler"
|
||||||
/>
|
/>
|
||||||
<div v-if="descriptionWarning" class="flex items-center gap-1.5 text-orange">
|
|
||||||
<TriangleAlertIcon class="my-auto" />
|
|
||||||
{{ descriptionWarning }}
|
|
||||||
</div>
|
|
||||||
<div class="input-group markdown-disclaimer">
|
<div class="input-group markdown-disclaimer">
|
||||||
<button
|
<button
|
||||||
:disabled="!hasChanges"
|
:disabled="!hasChanges"
|
||||||
@@ -42,8 +38,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { SaveIcon, TriangleAlertIcon } from "@modrinth/assets";
|
import { SaveIcon } from "@modrinth/assets";
|
||||||
import { countText, MIN_DESCRIPTION_CHARS } from "@modrinth/moderation";
|
|
||||||
import { MarkdownEditor } from "@modrinth/ui";
|
import { MarkdownEditor } from "@modrinth/ui";
|
||||||
import { type Project, type TeamMember, TeamMemberPermission } from "@modrinth/utils";
|
import { type Project, type TeamMember, TeamMemberPermission } from "@modrinth/utils";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
@@ -58,17 +53,6 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const description = ref(props.project.body);
|
const description = ref(props.project.body);
|
||||||
|
|
||||||
const descriptionWarning = computed(() => {
|
|
||||||
const text = description.value?.trim() || "";
|
|
||||||
const charCount = countText(text);
|
|
||||||
|
|
||||||
if (charCount < MIN_DESCRIPTION_CHARS) {
|
|
||||||
return `It's recommended to have a description with at least ${MIN_DESCRIPTION_CHARS} readable characters. (${charCount}/${MIN_DESCRIPTION_CHARS})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const patchRequestPayload = computed(() => {
|
const patchRequestPayload = computed(() => {
|
||||||
const payload: {
|
const payload: {
|
||||||
body?: string;
|
body?: string;
|
||||||
|
|||||||
@@ -82,10 +82,6 @@
|
|||||||
<label for="project-summary">
|
<label for="project-summary">
|
||||||
<span class="label__title">Summary</span>
|
<span class="label__title">Summary</span>
|
||||||
</label>
|
</label>
|
||||||
<div v-if="summaryWarning" class="my-2 flex items-center gap-1.5 text-orange">
|
|
||||||
<TriangleAlertIcon class="my-auto" />
|
|
||||||
{{ summaryWarning }}
|
|
||||||
</div>
|
|
||||||
<div class="textarea-wrapper summary-input">
|
<div class="textarea-wrapper summary-input">
|
||||||
<textarea
|
<textarea
|
||||||
id="project-summary"
|
id="project-summary"
|
||||||
@@ -244,18 +240,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { formatProjectStatus, formatProjectType } from "@modrinth/utils";
|
import { formatProjectStatus, formatProjectType } from "@modrinth/utils";
|
||||||
import {
|
import { UploadIcon, SaveIcon, TrashIcon, XIcon, IssuesIcon, CheckIcon } from "@modrinth/assets";
|
||||||
UploadIcon,
|
|
||||||
SaveIcon,
|
|
||||||
TrashIcon,
|
|
||||||
XIcon,
|
|
||||||
IssuesIcon,
|
|
||||||
CheckIcon,
|
|
||||||
TriangleAlertIcon,
|
|
||||||
} from "@modrinth/assets";
|
|
||||||
import { Multiselect } from "vue-multiselect";
|
import { Multiselect } from "vue-multiselect";
|
||||||
import { ConfirmModal, Avatar } from "@modrinth/ui";
|
import { ConfirmModal, Avatar } from "@modrinth/ui";
|
||||||
import { MIN_SUMMARY_CHARS } from "@modrinth/moderation";
|
|
||||||
import FileInput from "~/components/ui/FileInput.vue";
|
import FileInput from "~/components/ui/FileInput.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -313,17 +300,6 @@ const hasDeletePermission = computed(() => {
|
|||||||
return (props.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT;
|
return (props.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT;
|
||||||
});
|
});
|
||||||
|
|
||||||
const summaryWarning = computed(() => {
|
|
||||||
const text = summary.value?.trim() || "";
|
|
||||||
const charCount = text.length;
|
|
||||||
|
|
||||||
if (charCount < MIN_SUMMARY_CHARS) {
|
|
||||||
return `It's recommended to have a summary with at least ${MIN_SUMMARY_CHARS} characters. (${charCount}/${MIN_SUMMARY_CHARS})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const sideTypes = ["required", "optional", "unsupported"];
|
const sideTypes = ["required", "optional", "unsupported"];
|
||||||
|
|
||||||
const patchData = computed(() => {
|
const patchData = computed(() => {
|
||||||
|
|||||||
@@ -12,21 +12,6 @@
|
|||||||
A place for users to report bugs, issues, and concerns about your project.
|
A place for users to report bugs, issues, and concerns about your project.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<TriangleAlertIcon
|
|
||||||
v-if="isIssuesLinkShortener"
|
|
||||||
v-tooltip="`Use of link shorteners is prohibited.`"
|
|
||||||
class="size-6 animate-pulse text-orange"
|
|
||||||
/>
|
|
||||||
<TriangleAlertIcon
|
|
||||||
v-else-if="isIssuesDiscordUrl"
|
|
||||||
v-tooltip="`Discord invites are not appropriate for this link type.`"
|
|
||||||
class="size-6 animate-pulse text-orange"
|
|
||||||
/>
|
|
||||||
<TriangleAlertIcon
|
|
||||||
v-else-if="!isIssuesUrlCommon"
|
|
||||||
v-tooltip="`Link includes a domain which isn't common for this link type.`"
|
|
||||||
class="size-6 animate-pulse text-orange"
|
|
||||||
/>
|
|
||||||
<input
|
<input
|
||||||
id="project-issue-tracker"
|
id="project-issue-tracker"
|
||||||
v-model="issuesUrl"
|
v-model="issuesUrl"
|
||||||
@@ -46,21 +31,6 @@
|
|||||||
A page/repository containing the source code for your project
|
A page/repository containing the source code for your project
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<TriangleAlertIcon
|
|
||||||
v-if="isSourceLinkShortener"
|
|
||||||
v-tooltip="`Use of link shorteners is prohibited.`"
|
|
||||||
class="size-6 animate-pulse text-orange"
|
|
||||||
/>
|
|
||||||
<TriangleAlertIcon
|
|
||||||
v-else-if="isSourceDiscordUrl"
|
|
||||||
v-tooltip="`Discord invites are not appropriate for this link type.`"
|
|
||||||
class="size-6 animate-pulse text-orange"
|
|
||||||
/>
|
|
||||||
<TriangleAlertIcon
|
|
||||||
v-else-if="!isSourceUrlCommon"
|
|
||||||
v-tooltip="`Link includes a domain which isn't common for this link type.`"
|
|
||||||
class="size-6 animate-pulse text-orange"
|
|
||||||
/>
|
|
||||||
<input
|
<input
|
||||||
id="project-source-code"
|
id="project-source-code"
|
||||||
v-model="sourceUrl"
|
v-model="sourceUrl"
|
||||||
@@ -80,16 +50,6 @@
|
|||||||
A page containing information, documentation, and help for the project.
|
A page containing information, documentation, and help for the project.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<TriangleAlertIcon
|
|
||||||
v-if="isWikiLinkShortener"
|
|
||||||
v-tooltip="`Use of link shorteners is prohibited.`"
|
|
||||||
class="size-6 animate-pulse text-orange"
|
|
||||||
/>
|
|
||||||
<TriangleAlertIcon
|
|
||||||
v-else-if="isWikiDiscordUrl"
|
|
||||||
v-tooltip="`Discord invites are not appropriate for this link type.`"
|
|
||||||
class="size-6 animate-pulse text-orange"
|
|
||||||
/>
|
|
||||||
<input
|
<input
|
||||||
id="project-wiki-page"
|
id="project-wiki-page"
|
||||||
v-model="wikiUrl"
|
v-model="wikiUrl"
|
||||||
@@ -104,16 +64,6 @@
|
|||||||
<span class="label__title">Discord invite</span>
|
<span class="label__title">Discord invite</span>
|
||||||
<span class="label__description"> An invitation link to your Discord server. </span>
|
<span class="label__description"> An invitation link to your Discord server. </span>
|
||||||
</label>
|
</label>
|
||||||
<TriangleAlertIcon
|
|
||||||
v-if="isDiscordLinkShortener"
|
|
||||||
v-tooltip="`Use of link shorteners is prohibited.`"
|
|
||||||
class="size-6 animate-pulse text-orange"
|
|
||||||
/>
|
|
||||||
<TriangleAlertIcon
|
|
||||||
v-else-if="!isDiscordUrlCommon"
|
|
||||||
v-tooltip="`You're using a link which isn't common for this link type.`"
|
|
||||||
class="size-6 animate-pulse text-orange"
|
|
||||||
/>
|
|
||||||
<input
|
<input
|
||||||
id="project-discord-invite"
|
id="project-discord-invite"
|
||||||
v-model="discordUrl"
|
v-model="discordUrl"
|
||||||
@@ -173,13 +123,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { DropdownSelect } from "@modrinth/ui";
|
import { DropdownSelect } from "@modrinth/ui";
|
||||||
import { SaveIcon, TriangleAlertIcon } from "@modrinth/assets";
|
import { SaveIcon } from "@modrinth/assets";
|
||||||
import {
|
|
||||||
isCommonUrl,
|
|
||||||
isDiscordUrl,
|
|
||||||
isLinkShortener,
|
|
||||||
commonLinkDomains,
|
|
||||||
} from "@modrinth/moderation";
|
|
||||||
|
|
||||||
const tags = useTags();
|
const tags = useTags();
|
||||||
|
|
||||||
@@ -209,46 +153,6 @@ const sourceUrl = ref(props.project.source_url);
|
|||||||
const wikiUrl = ref(props.project.wiki_url);
|
const wikiUrl = ref(props.project.wiki_url);
|
||||||
const discordUrl = ref(props.project.discord_url);
|
const discordUrl = ref(props.project.discord_url);
|
||||||
|
|
||||||
const isIssuesUrlCommon = computed(() => {
|
|
||||||
if (!issuesUrl.value || issuesUrl.value.trim().length === 0) return true;
|
|
||||||
return isCommonUrl(issuesUrl.value, commonLinkDomains.issues);
|
|
||||||
});
|
|
||||||
|
|
||||||
const isSourceUrlCommon = computed(() => {
|
|
||||||
if (!sourceUrl.value || sourceUrl.value.trim().length === 0) return true;
|
|
||||||
return isCommonUrl(sourceUrl.value, commonLinkDomains.source);
|
|
||||||
});
|
|
||||||
|
|
||||||
const isDiscordUrlCommon = computed(() => {
|
|
||||||
if (!discordUrl.value || discordUrl.value.trim().length === 0) return true;
|
|
||||||
return isCommonUrl(discordUrl.value, commonLinkDomains.discord);
|
|
||||||
});
|
|
||||||
|
|
||||||
const isIssuesDiscordUrl = computed(() => {
|
|
||||||
return isDiscordUrl(issuesUrl.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const isSourceDiscordUrl = computed(() => {
|
|
||||||
return isDiscordUrl(sourceUrl.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const isWikiDiscordUrl = computed(() => {
|
|
||||||
return isDiscordUrl(wikiUrl.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const isIssuesLinkShortener = computed(() => {
|
|
||||||
return isLinkShortener(issuesUrl.value);
|
|
||||||
});
|
|
||||||
const isSourceLinkShortener = computed(() => {
|
|
||||||
return isLinkShortener(sourceUrl.value);
|
|
||||||
});
|
|
||||||
const isWikiLinkShortener = computed(() => {
|
|
||||||
return isLinkShortener(wikiUrl.value);
|
|
||||||
});
|
|
||||||
const isDiscordLinkShortener = computed(() => {
|
|
||||||
return isLinkShortener(discordUrl.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const rawDonationLinks = JSON.parse(JSON.stringify(props.project.donation_urls));
|
const rawDonationLinks = JSON.parse(JSON.stringify(props.project.donation_urls));
|
||||||
rawDonationLinks.push({
|
rawDonationLinks.push({
|
||||||
id: null,
|
id: null,
|
||||||
|
|||||||
@@ -6,31 +6,11 @@
|
|||||||
<span class="label__title size-card-header">Tags</span>
|
<span class="label__title size-card-header">Tags</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="tooManyTagsWarning && !allTagsSelectedWarning"
|
|
||||||
class="my-2 flex items-center gap-1.5 text-orange"
|
|
||||||
>
|
|
||||||
<TriangleAlertIcon class="my-auto" />
|
|
||||||
{{ tooManyTagsWarning }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="multipleResolutionTagsWarning" class="my-2 flex items-center gap-1.5 text-orange">
|
|
||||||
<TriangleAlertIcon class="my-auto" />
|
|
||||||
{{ multipleResolutionTagsWarning }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="allTagsSelectedWarning" class="my-2 flex items-center gap-1.5 text-red">
|
|
||||||
<TriangleAlertIcon class="my-auto" />
|
|
||||||
<span>{{ allTagsSelectedWarning }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Accurate tagging is important to help people find your
|
Accurate tagging is important to help people find your
|
||||||
{{ formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags
|
{{ formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags
|
||||||
that apply.
|
that apply.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p v-if="project.versions.length === 0" class="known-errors">
|
<p v-if="project.versions.length === 0" class="known-errors">
|
||||||
Please upload a version first in order to select tags!
|
Please upload a version first in order to select tags!
|
||||||
</p>
|
</p>
|
||||||
@@ -132,69 +112,70 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { computed, ref } from "vue";
|
import { StarIcon, SaveIcon } from "@modrinth/assets";
|
||||||
import { StarIcon, SaveIcon, TriangleAlertIcon } from "@modrinth/assets";
|
import { formatCategory, formatCategoryHeader, formatProjectType } from "@modrinth/utils";
|
||||||
import {
|
|
||||||
formatCategory,
|
|
||||||
formatCategoryHeader,
|
|
||||||
formatProjectType,
|
|
||||||
sortedCategories,
|
|
||||||
type Project,
|
|
||||||
} from "@modrinth/utils";
|
|
||||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||||
|
|
||||||
interface Category {
|
export default defineNuxtComponent({
|
||||||
name: string;
|
components: {
|
||||||
header: string;
|
Checkbox,
|
||||||
icon?: string;
|
SaveIcon,
|
||||||
project_type: string;
|
StarIcon,
|
||||||
}
|
},
|
||||||
|
props: {
|
||||||
interface Props {
|
project: {
|
||||||
project: Project & {
|
type: Object,
|
||||||
actualProjectType: string;
|
default() {
|
||||||
};
|
return {};
|
||||||
allMembers?: any[];
|
},
|
||||||
currentMember?: any;
|
},
|
||||||
patchProject?: (data: any) => void;
|
allMembers: {
|
||||||
}
|
type: Array,
|
||||||
|
default() {
|
||||||
const tags = useTags();
|
return [];
|
||||||
|
},
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
},
|
||||||
allMembers: () => [],
|
currentMember: {
|
||||||
currentMember: null,
|
type: Object,
|
||||||
patchProject: () => {
|
default() {
|
||||||
addNotification({
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
patchProject: {
|
||||||
|
type: Function,
|
||||||
|
default() {
|
||||||
|
return () => {
|
||||||
|
this.$notify({
|
||||||
|
group: "main",
|
||||||
title: "An error occurred",
|
title: "An error occurred",
|
||||||
text: "Patch project function not found",
|
text: "Patch project function not found",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
},
|
||||||
const selectedTags = ref<Category[]>(
|
data() {
|
||||||
sortedCategories(tags.value).filter(
|
return {
|
||||||
(x: Category) =>
|
selectedTags: this.$sortedCategories().filter(
|
||||||
x.project_type === props.project.actualProjectType &&
|
(x) =>
|
||||||
(props.project.categories.includes(x.name) ||
|
x.project_type === this.project.actualProjectType &&
|
||||||
props.project.additional_categories.includes(x.name)),
|
(this.project.categories.includes(x.name) ||
|
||||||
|
this.project.additional_categories.includes(x.name)),
|
||||||
),
|
),
|
||||||
);
|
featuredTags: this.$sortedCategories().filter(
|
||||||
|
(x) =>
|
||||||
const featuredTags = ref<Category[]>(
|
x.project_type === this.project.actualProjectType &&
|
||||||
sortedCategories(tags.value).filter(
|
this.project.categories.includes(x.name),
|
||||||
(x: Category) =>
|
|
||||||
x.project_type === props.project.actualProjectType &&
|
|
||||||
props.project.categories.includes(x.name),
|
|
||||||
),
|
),
|
||||||
);
|
};
|
||||||
|
},
|
||||||
const categoryLists = computed(() => {
|
computed: {
|
||||||
const lists: Record<string, Category[]> = {};
|
categoryLists() {
|
||||||
sortedCategories(tags.value).forEach((x: Category) => {
|
const lists = {};
|
||||||
if (x.project_type === props.project.actualProjectType) {
|
this.$sortedCategories().forEach((x) => {
|
||||||
|
if (x.project_type === this.project.actualProjectType) {
|
||||||
const header = x.header;
|
const header = x.header;
|
||||||
if (!lists[header]) {
|
if (!lists[header]) {
|
||||||
lists[header] = [];
|
lists[header] = [];
|
||||||
@@ -203,117 +184,73 @@ const categoryLists = computed(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return lists;
|
return lists;
|
||||||
});
|
},
|
||||||
|
patchData() {
|
||||||
const tooManyTagsWarning = computed(() => {
|
const data = {};
|
||||||
const tagCount = selectedTags.value.length;
|
|
||||||
if (tagCount > 8) {
|
|
||||||
return `You've selected ${tagCount} tags. Consider reducing to 8 or fewer to keep your project focused and easier to discover.`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const multipleResolutionTagsWarning = computed(() => {
|
|
||||||
if (props.project.project_type !== "resourcepack") return null;
|
|
||||||
|
|
||||||
const resolutionTags = selectedTags.value.filter((tag) =>
|
|
||||||
["8x-", "16x", "32x", "48x", "64x", "128x", "256x", "512x+"].includes(tag.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (resolutionTags.length > 1) {
|
|
||||||
return `You've selected ${resolutionTags.length} resolution tags (${resolutionTags
|
|
||||||
.map((t) => t.name)
|
|
||||||
.join(", ")
|
|
||||||
.replace("8x-", "8x or lower")
|
|
||||||
.replace(
|
|
||||||
"512x+",
|
|
||||||
"512x or higher",
|
|
||||||
)}). Resource packs should typically only have one resolution tag.`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const allTagsSelectedWarning = computed(() => {
|
|
||||||
const categoriesForProjectType = sortedCategories(tags.value).filter(
|
|
||||||
(x: Category) => x.project_type === props.project.actualProjectType,
|
|
||||||
);
|
|
||||||
const totalSelectedTags = selectedTags.value.length;
|
|
||||||
|
|
||||||
if (
|
|
||||||
totalSelectedTags === categoriesForProjectType.length &&
|
|
||||||
categoriesForProjectType.length > 0
|
|
||||||
) {
|
|
||||||
return `You've selected all ${categoriesForProjectType.length} available tags. Please select only the tags that truly apply to your project.`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const patchData = computed(() => {
|
|
||||||
const data: Record<string, string[]> = {};
|
|
||||||
|
|
||||||
// Promote selected categories to featured if there are less than 3 featured
|
// Promote selected categories to featured if there are less than 3 featured
|
||||||
const newFeaturedTags = featuredTags.value.slice();
|
const newFeaturedTags = this.featuredTags.slice();
|
||||||
if (newFeaturedTags.length < 1 && selectedTags.value.length > newFeaturedTags.length) {
|
if (newFeaturedTags.length < 1 && this.selectedTags.length > newFeaturedTags.length) {
|
||||||
const nonFeaturedCategories = selectedTags.value.filter((x) => !newFeaturedTags.includes(x));
|
const nonFeaturedCategories = this.selectedTags.filter((x) => !newFeaturedTags.includes(x));
|
||||||
|
|
||||||
nonFeaturedCategories
|
nonFeaturedCategories
|
||||||
.slice(0, Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length))
|
.slice(0, Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length))
|
||||||
.forEach((x) => newFeaturedTags.push(x));
|
.forEach((x) => newFeaturedTags.push(x));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert selected and featured categories to backend-usable arrays
|
// Convert selected and featured categories to backend-usable arrays
|
||||||
const categories = newFeaturedTags.map((x) => x.name);
|
const categories = newFeaturedTags.map((x) => x.name);
|
||||||
const additionalCategories = selectedTags.value
|
const additionalCategories = this.selectedTags
|
||||||
.filter((x) => !newFeaturedTags.includes(x))
|
.filter((x) => !newFeaturedTags.includes(x))
|
||||||
.map((x) => x.name);
|
.map((x) => x.name);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
categories.length !== props.project.categories.length ||
|
categories.length !== this.project.categories.length ||
|
||||||
categories.some((value) => !props.project.categories.includes(value))
|
categories.some((value) => !this.project.categories.includes(value))
|
||||||
) {
|
) {
|
||||||
data.categories = categories;
|
data.categories = categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
additionalCategories.length !== props.project.additional_categories.length ||
|
additionalCategories.length !== this.project.additional_categories.length ||
|
||||||
additionalCategories.some((value) => !props.project.additional_categories.includes(value))
|
additionalCategories.some((value) => !this.project.additional_categories.includes(value))
|
||||||
) {
|
) {
|
||||||
data.additional_categories = additionalCategories;
|
data.additional_categories = additionalCategories;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
});
|
},
|
||||||
|
hasChanges() {
|
||||||
const hasChanges = computed(() => {
|
return Object.keys(this.patchData).length > 0;
|
||||||
return Object.keys(patchData.value).length > 0;
|
},
|
||||||
});
|
},
|
||||||
|
methods: {
|
||||||
const toggleCategory = (category: Category) => {
|
formatProjectType,
|
||||||
if (selectedTags.value.includes(category)) {
|
formatCategoryHeader,
|
||||||
selectedTags.value = selectedTags.value.filter((x) => x !== category);
|
formatCategory,
|
||||||
if (featuredTags.value.includes(category)) {
|
toggleCategory(category) {
|
||||||
featuredTags.value = featuredTags.value.filter((x) => x !== category);
|
if (this.selectedTags.includes(category)) {
|
||||||
|
this.selectedTags = this.selectedTags.filter((x) => x !== category);
|
||||||
|
if (this.featuredTags.includes(category)) {
|
||||||
|
this.featuredTags = this.featuredTags.filter((x) => x !== category);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedTags.value.push(category);
|
this.selectedTags.push(category);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
toggleFeaturedCategory(category) {
|
||||||
const toggleFeaturedCategory = (category: Category) => {
|
if (this.featuredTags.includes(category)) {
|
||||||
if (featuredTags.value.includes(category)) {
|
this.featuredTags = this.featuredTags.filter((x) => x !== category);
|
||||||
featuredTags.value = featuredTags.value.filter((x) => x !== category);
|
|
||||||
} else {
|
} else {
|
||||||
featuredTags.value.push(category);
|
this.featuredTags.push(category);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
saveChanges() {
|
||||||
const saveChanges = () => {
|
if (this.hasChanges) {
|
||||||
if (hasChanges.value) {
|
this.patchProject(this.patchData);
|
||||||
props.patchProject(patchData.value);
|
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.label__title {
|
.label__title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -322,13 +322,12 @@ pub async fn is_visible_collection(
|
|||||||
} else {
|
} else {
|
||||||
!collection_data.status.is_hidden()
|
!collection_data.status.is_hidden()
|
||||||
}) && !collection_data.projects.is_empty();
|
}) && !collection_data.projects.is_empty();
|
||||||
if let Some(user) = &user_option {
|
if let Some(user) = &user_option
|
||||||
if !authorized
|
&& !authorized
|
||||||
&& (user.role.is_mod() || user.id == collection_data.user_id.into())
|
&& (user.role.is_mod() || user.id == collection_data.user_id.into())
|
||||||
{
|
{
|
||||||
authorized = true;
|
authorized = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(authorized)
|
Ok(authorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,12 +355,12 @@ pub async fn filter_visible_collections(
|
|||||||
|
|
||||||
for collection in check_collections {
|
for collection in check_collections {
|
||||||
// Collections are simple- if we are the owner or a mod, we can see it
|
// Collections are simple- if we are the owner or a mod, we can see it
|
||||||
if let Some(user) = user_option {
|
if let Some(user) = user_option
|
||||||
if user.role.is_mod() || user.id == collection.user_id.into() {
|
&& (user.role.is_mod() || user.id == collection.user_id.into())
|
||||||
|
{
|
||||||
return_collections.push(collection.into());
|
return_collections.push(collection.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(return_collections)
|
Ok(return_collections)
|
||||||
}
|
}
|
||||||
@@ -95,11 +95,11 @@ impl DBFlow {
|
|||||||
redis: &RedisPool,
|
redis: &RedisPool,
|
||||||
) -> Result<Option<DBFlow>, DatabaseError> {
|
) -> Result<Option<DBFlow>, DatabaseError> {
|
||||||
let flow = Self::get(id, redis).await?;
|
let flow = Self::get(id, redis).await?;
|
||||||
if let Some(flow) = flow.as_ref() {
|
if let Some(flow) = flow.as_ref()
|
||||||
if predicate(flow) {
|
&& predicate(flow)
|
||||||
|
{
|
||||||
Self::remove(id, redis).await?;
|
Self::remove(id, redis).await?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(flow)
|
Ok(flow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -801,18 +801,19 @@ impl VersionField {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(count) = countable {
|
if let Some(count) = countable {
|
||||||
if let Some(min) = loader_field.min_val {
|
if let Some(min) = loader_field.min_val
|
||||||
if count < min {
|
&& count < min
|
||||||
|
{
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Provided value '{v}' for {field_name} is less than the minimum of {min}",
|
"Provided value '{v}' for {field_name} is less than the minimum of {min}",
|
||||||
v = serde_json::to_string(&value).unwrap_or_default(),
|
v = serde_json::to_string(&value).unwrap_or_default(),
|
||||||
field_name = loader_field.field,
|
field_name = loader_field.field,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(max) = loader_field.max_val {
|
if let Some(max) = loader_field.max_val
|
||||||
if count > max {
|
&& count > max
|
||||||
|
{
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Provided value '{v}' for {field_name} is greater than the maximum of {max}",
|
"Provided value '{v}' for {field_name} is greater than the maximum of {max}",
|
||||||
v = serde_json::to_string(&value).unwrap_or_default(),
|
v = serde_json::to_string(&value).unwrap_or_default(),
|
||||||
@@ -820,7 +821,6 @@ impl VersionField {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(VersionField {
|
Ok(VersionField {
|
||||||
version_id,
|
version_id,
|
||||||
|
|||||||
@@ -483,8 +483,9 @@ impl DBTeamMember {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(accepted) = new_accepted {
|
if let Some(accepted) = new_accepted
|
||||||
if accepted {
|
&& accepted
|
||||||
|
{
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE team_members
|
UPDATE team_members
|
||||||
@@ -497,7 +498,6 @@ impl DBTeamMember {
|
|||||||
.execute(&mut **transaction)
|
.execute(&mut **transaction)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(payouts_split) = new_payouts_split {
|
if let Some(payouts_split) = new_payouts_split {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
|
|||||||
@@ -353,11 +353,11 @@ impl RedisPool {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (idx, key) in fetch_ids.into_iter().enumerate() {
|
for (idx, key) in fetch_ids.into_iter().enumerate() {
|
||||||
if let Some(locked) = results.get(idx) {
|
if let Some(locked) = results.get(idx)
|
||||||
if locked.is_none() {
|
&& locked.is_none()
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((key, raw_key)) = ids.remove(&key) {
|
if let Some((key, raw_key)) = ids.remove(&key) {
|
||||||
if let Some(val) = expired_values.remove(&key) {
|
if let Some(val) = expired_values.remove(&key) {
|
||||||
|
|||||||
@@ -334,19 +334,15 @@ impl From<Version> for LegacyVersion {
|
|||||||
// the v2 loaders are whatever the corresponding loader fields are
|
// the v2 loaders are whatever the corresponding loader fields are
|
||||||
let mut loaders =
|
let mut loaders =
|
||||||
data.loaders.into_iter().map(|l| l.0).collect::<Vec<_>>();
|
data.loaders.into_iter().map(|l| l.0).collect::<Vec<_>>();
|
||||||
if loaders.contains(&"mrpack".to_string()) {
|
if loaders.contains(&"mrpack".to_string())
|
||||||
if let Some((_, mrpack_loaders)) = data
|
&& let Some((_, mrpack_loaders)) = data
|
||||||
.fields
|
.fields
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|(key, _)| key == "mrpack_loaders")
|
.find(|(key, _)| key == "mrpack_loaders")
|
||||||
{
|
&& let Ok(mrpack_loaders) = serde_json::from_value(mrpack_loaders)
|
||||||
if let Ok(mrpack_loaders) =
|
|
||||||
serde_json::from_value(mrpack_loaders)
|
|
||||||
{
|
{
|
||||||
loaders = mrpack_loaders;
|
loaders = mrpack_loaders;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
let loaders = loaders.into_iter().map(Loader).collect::<Vec<_>>();
|
let loaders = loaders.into_iter().map(Loader).collect::<Vec<_>>();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ impl LegacyResultSearchProject {
|
|||||||
pub fn from(result_search_project: ResultSearchProject) -> Self {
|
pub fn from(result_search_project: ResultSearchProject) -> Self {
|
||||||
let mut categories = result_search_project.categories;
|
let mut categories = result_search_project.categories;
|
||||||
categories.extend(result_search_project.loaders.clone());
|
categories.extend(result_search_project.loaders.clone());
|
||||||
if categories.contains(&"mrpack".to_string()) {
|
if categories.contains(&"mrpack".to_string())
|
||||||
if let Some(mrpack_loaders) = result_search_project
|
&& let Some(mrpack_loaders) = result_search_project
|
||||||
.project_loader_fields
|
.project_loader_fields
|
||||||
.get("mrpack_loaders")
|
.get("mrpack_loaders")
|
||||||
{
|
{
|
||||||
@@ -56,11 +56,10 @@ impl LegacyResultSearchProject {
|
|||||||
);
|
);
|
||||||
categories.retain(|c| c != "mrpack");
|
categories.retain(|c| c != "mrpack");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let mut display_categories = result_search_project.display_categories;
|
let mut display_categories = result_search_project.display_categories;
|
||||||
display_categories.extend(result_search_project.loaders);
|
display_categories.extend(result_search_project.loaders);
|
||||||
if display_categories.contains(&"mrpack".to_string()) {
|
if display_categories.contains(&"mrpack".to_string())
|
||||||
if let Some(mrpack_loaders) = result_search_project
|
&& let Some(mrpack_loaders) = result_search_project
|
||||||
.project_loader_fields
|
.project_loader_fields
|
||||||
.get("mrpack_loaders")
|
.get("mrpack_loaders")
|
||||||
{
|
{
|
||||||
@@ -72,7 +71,6 @@ impl LegacyResultSearchProject {
|
|||||||
);
|
);
|
||||||
display_categories.retain(|c| c != "mrpack");
|
display_categories.retain(|c| c != "mrpack");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Sort then remove duplicates
|
// Sort then remove duplicates
|
||||||
categories.sort();
|
categories.sort();
|
||||||
|
|||||||
@@ -166,12 +166,12 @@ impl From<ProjectQueryResult> for Project {
|
|||||||
Ok(spdx_expr) => {
|
Ok(spdx_expr) => {
|
||||||
let mut vec: Vec<&str> = Vec::new();
|
let mut vec: Vec<&str> = Vec::new();
|
||||||
for node in spdx_expr.iter() {
|
for node in spdx_expr.iter() {
|
||||||
if let spdx::expression::ExprNode::Req(req) = node {
|
if let spdx::expression::ExprNode::Req(req) = node
|
||||||
if let Some(id) = req.req.license.id() {
|
&& let Some(id) = req.req.license.id()
|
||||||
|
{
|
||||||
vec.push(id.full_name);
|
vec.push(id.full_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// spdx crate returns AND/OR operations in postfix order
|
// spdx crate returns AND/OR operations in postfix order
|
||||||
// and it would be a lot more effort to make it actually in order
|
// and it would be a lot more effort to make it actually in order
|
||||||
// so let's just ignore that and make them comma-separated
|
// so let's just ignore that and make them comma-separated
|
||||||
|
|||||||
@@ -51,17 +51,17 @@ impl ProjectPermissions {
|
|||||||
return Some(ProjectPermissions::all());
|
return Some(ProjectPermissions::all());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(member) = project_team_member {
|
if let Some(member) = project_team_member
|
||||||
if member.accepted {
|
&& member.accepted
|
||||||
|
{
|
||||||
return Some(member.permissions);
|
return Some(member.permissions);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(member) = organization_team_member {
|
if let Some(member) = organization_team_member
|
||||||
if member.accepted {
|
&& member.accepted
|
||||||
|
{
|
||||||
return Some(member.permissions);
|
return Some(member.permissions);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if role.is_mod() {
|
if role.is_mod() {
|
||||||
Some(
|
Some(
|
||||||
@@ -107,11 +107,11 @@ impl OrganizationPermissions {
|
|||||||
return Some(OrganizationPermissions::all());
|
return Some(OrganizationPermissions::all());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(member) = team_member {
|
if let Some(member) = team_member
|
||||||
if member.accepted {
|
&& member.accepted
|
||||||
|
{
|
||||||
return member.organization_permissions;
|
return member.organization_permissions;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if role.is_mod() {
|
if role.is_mod() {
|
||||||
return Some(
|
return Some(
|
||||||
OrganizationPermissions::EDIT_DETAILS
|
OrganizationPermissions::EDIT_DETAILS
|
||||||
|
|||||||
@@ -45,20 +45,18 @@ impl MaxMindIndexer {
|
|||||||
|
|
||||||
if let Ok(entries) = archive.entries() {
|
if let Ok(entries) = archive.entries() {
|
||||||
for mut file in entries.flatten() {
|
for mut file in entries.flatten() {
|
||||||
if let Ok(path) = file.header().path() {
|
if let Ok(path) = file.header().path()
|
||||||
if path.extension().and_then(|x| x.to_str()) == Some("mmdb")
|
&& path.extension().and_then(|x| x.to_str()) == Some("mmdb")
|
||||||
{
|
{
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
file.read_to_end(&mut buf).unwrap();
|
file.read_to_end(&mut buf).unwrap();
|
||||||
|
|
||||||
let reader =
|
let reader = maxminddb::Reader::from_source(buf).unwrap();
|
||||||
maxminddb::Reader::from_source(buf).unwrap();
|
|
||||||
|
|
||||||
return Ok(Some(reader));
|
return Ok(Some(reader));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if should_panic {
|
if should_panic {
|
||||||
panic!(
|
panic!(
|
||||||
|
|||||||
@@ -371,8 +371,8 @@ impl AutomatedModerationQueue {
|
|||||||
for file in
|
for file in
|
||||||
files.iter().filter(|x| x.version_id == version.id.into())
|
files.iter().filter(|x| x.version_id == version.id.into())
|
||||||
{
|
{
|
||||||
if let Some(hash) = file.hashes.get("sha1") {
|
if let Some(hash) = file.hashes.get("sha1")
|
||||||
if let Some((index, (sha1, _, file_name, _))) = hashes
|
&& let Some((index, (sha1, _, file_name, _))) = hashes
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find(|(_, (value, _, _, _))| value == hash)
|
.find(|(_, (value, _, _, _))| value == hash)
|
||||||
@@ -384,7 +384,6 @@ impl AutomatedModerationQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// All files are on Modrinth, so we don't send any messages
|
// All files are on Modrinth, so we don't send any messages
|
||||||
if hashes.is_empty() {
|
if hashes.is_empty() {
|
||||||
@@ -420,13 +419,12 @@ impl AutomatedModerationQueue {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for row in rows {
|
for row in rows {
|
||||||
if let Some(sha1) = row.sha1 {
|
if let Some(sha1) = row.sha1
|
||||||
if let Some((index, (sha1, _, file_name, _))) = hashes.iter().enumerate().find(|(_, (value, _, _, _))| value == &sha1) {
|
&& let Some((index, (sha1, _, file_name, _))) = hashes.iter().enumerate().find(|(_, (value, _, _, _))| value == &sha1) {
|
||||||
final_hashes.insert(sha1.clone(), IdentifiedFile { file_name: file_name.clone(), status: ApprovalType::from_string(&row.status).unwrap_or(ApprovalType::Unidentified) });
|
final_hashes.insert(sha1.clone(), IdentifiedFile { file_name: file_name.clone(), status: ApprovalType::from_string(&row.status).unwrap_or(ApprovalType::Unidentified) });
|
||||||
hashes.remove(index);
|
hashes.remove(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if hashes.is_empty() {
|
if hashes.is_empty() {
|
||||||
let metadata = MissingMetadata {
|
let metadata = MissingMetadata {
|
||||||
@@ -499,8 +497,8 @@ impl AutomatedModerationQueue {
|
|||||||
let mut insert_ids = Vec::new();
|
let mut insert_ids = Vec::new();
|
||||||
|
|
||||||
for row in rows {
|
for row in rows {
|
||||||
if let Some((curse_index, (hash, _flame_id))) = flame_files.iter().enumerate().find(|(_, x)| Some(x.1 as i32) == row.flame_project_id) {
|
if let Some((curse_index, (hash, _flame_id))) = flame_files.iter().enumerate().find(|(_, x)| Some(x.1 as i32) == row.flame_project_id)
|
||||||
if let Some((index, (sha1, _, file_name, _))) = hashes.iter().enumerate().find(|(_, (value, _, _, _))| value == hash) {
|
&& let Some((index, (sha1, _, file_name, _))) = hashes.iter().enumerate().find(|(_, (value, _, _, _))| value == hash) {
|
||||||
final_hashes.insert(sha1.clone(), IdentifiedFile {
|
final_hashes.insert(sha1.clone(), IdentifiedFile {
|
||||||
file_name: file_name.clone(),
|
file_name: file_name.clone(),
|
||||||
status: ApprovalType::from_string(&row.status).unwrap_or(ApprovalType::Unidentified),
|
status: ApprovalType::from_string(&row.status).unwrap_or(ApprovalType::Unidentified),
|
||||||
@@ -513,7 +511,6 @@ impl AutomatedModerationQueue {
|
|||||||
flame_files.remove(curse_index);
|
flame_files.remove(curse_index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !insert_ids.is_empty() && !insert_hashes.is_empty() {
|
if !insert_ids.is_empty() && !insert_hashes.is_empty() {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
@@ -581,8 +578,8 @@ impl AutomatedModerationQueue {
|
|||||||
for (sha1, _pack_file, file_name, _mumur2) in hashes {
|
for (sha1, _pack_file, file_name, _mumur2) in hashes {
|
||||||
let flame_file = flame_files.iter().find(|x| x.0 == sha1);
|
let flame_file = flame_files.iter().find(|x| x.0 == sha1);
|
||||||
|
|
||||||
if let Some((_, flame_project_id)) = flame_file {
|
if let Some((_, flame_project_id)) = flame_file
|
||||||
if let Some(project) = flame_projects.iter().find(|x| &x.id == flame_project_id) {
|
&& let Some(project) = flame_projects.iter().find(|x| &x.id == flame_project_id) {
|
||||||
missing_metadata.flame_files.insert(sha1, MissingMetadataFlame {
|
missing_metadata.flame_files.insert(sha1, MissingMetadataFlame {
|
||||||
title: project.name.clone(),
|
title: project.name.clone(),
|
||||||
file_name,
|
file_name,
|
||||||
@@ -592,7 +589,6 @@ impl AutomatedModerationQueue {
|
|||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
missing_metadata.unknown_files.insert(sha1, file_name);
|
missing_metadata.unknown_files.insert(sha1, file_name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,17 +257,17 @@ impl PayoutsQueue {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !status.is_success() {
|
if !status.is_success()
|
||||||
if let Some(obj) = value.as_object() {
|
&& let Some(obj) = value.as_object()
|
||||||
|
{
|
||||||
if let Some(array) = obj.get("errors") {
|
if let Some(array) = obj.get("errors") {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct TremendousError {
|
struct TremendousError {
|
||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
let err = serde_json::from_value::<TremendousError>(
|
let err =
|
||||||
array.clone(),
|
serde_json::from_value::<TremendousError>(array.clone())
|
||||||
)
|
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
ApiError::Payments(
|
ApiError::Payments(
|
||||||
"could not retrieve Tremendous error json body"
|
"could not retrieve Tremendous error json body"
|
||||||
@@ -282,7 +282,6 @@ impl PayoutsQueue {
|
|||||||
"could not retrieve Tremendous error body".to_string(),
|
"could not retrieve Tremendous error body".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(serde_json::from_value(value)?)
|
Ok(serde_json::from_value(value)?)
|
||||||
}
|
}
|
||||||
@@ -449,11 +448,11 @@ impl PayoutsQueue {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// we do not support interval gift cards with non US based currencies since we cannot do currency conversions properly
|
// we do not support interval gift cards with non US based currencies since we cannot do currency conversions properly
|
||||||
if let PayoutInterval::Fixed { .. } = method.interval {
|
if let PayoutInterval::Fixed { .. } = method.interval
|
||||||
if !product.currency_codes.contains(&"USD".to_string()) {
|
&& !product.currency_codes.contains(&"USD".to_string())
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
methods.push(method);
|
methods.push(method);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,8 +286,9 @@ pub async fn refund_charge(
|
|||||||
.upsert(&mut transaction)
|
.upsert(&mut transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if body.0.unprovision.unwrap_or(false) {
|
if body.0.unprovision.unwrap_or(false)
|
||||||
if let Some(subscription_id) = charge.subscription_id {
|
&& let Some(subscription_id) = charge.subscription_id
|
||||||
|
{
|
||||||
let open_charge =
|
let open_charge =
|
||||||
DBCharge::get_open_subscription(subscription_id, &**pool)
|
DBCharge::get_open_subscription(subscription_id, &**pool)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -298,7 +299,6 @@ pub async fn refund_charge(
|
|||||||
open_charge.upsert(&mut transaction).await?;
|
open_charge.upsert(&mut transaction).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
}
|
}
|
||||||
@@ -392,19 +392,18 @@ pub async fn edit_subscription(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(interval) = &edit_subscription.interval {
|
if let Some(interval) = &edit_subscription.interval
|
||||||
if let Price::Recurring { intervals } = ¤t_price.prices {
|
&& let Price::Recurring { intervals } = ¤t_price.prices
|
||||||
|
{
|
||||||
if let Some(price) = intervals.get(interval) {
|
if let Some(price) = intervals.get(interval) {
|
||||||
open_charge.subscription_interval = Some(*interval);
|
open_charge.subscription_interval = Some(*interval);
|
||||||
open_charge.amount = *price as i64;
|
open_charge.amount = *price as i64;
|
||||||
} else {
|
} else {
|
||||||
return Err(ApiError::InvalidInput(
|
return Err(ApiError::InvalidInput(
|
||||||
"Interval is not valid for this subscription!"
|
"Interval is not valid for this subscription!".to_string(),
|
||||||
.to_string(),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let intent = if let Some(product_id) = &edit_subscription.product {
|
let intent = if let Some(product_id) = &edit_subscription.product {
|
||||||
let product_price =
|
let product_price =
|
||||||
@@ -1225,8 +1224,9 @@ pub async fn initiate_payment(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Price::Recurring { .. } = price_item.prices {
|
if let Price::Recurring { .. } = price_item.prices
|
||||||
if product.unitary {
|
&& product.unitary
|
||||||
|
{
|
||||||
let user_subscriptions =
|
let user_subscriptions =
|
||||||
user_subscription_item::DBUserSubscription::get_all_user(
|
user_subscription_item::DBUserSubscription::get_all_user(
|
||||||
user.id.into(),
|
user.id.into(),
|
||||||
@@ -1234,13 +1234,11 @@ pub async fn initiate_payment(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let user_products =
|
let user_products = product_item::DBProductPrice::get_many(
|
||||||
product_item::DBProductPrice::get_many(
|
|
||||||
&user_subscriptions
|
&user_subscriptions
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| {
|
.filter(|x| {
|
||||||
x.status
|
x.status == SubscriptionStatus::Provisioned
|
||||||
== SubscriptionStatus::Provisioned
|
|
||||||
})
|
})
|
||||||
.map(|x| x.price_id)
|
.map(|x| x.price_id)
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
@@ -1258,7 +1256,6 @@ pub async fn initiate_payment(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
(
|
(
|
||||||
price as i64,
|
price as i64,
|
||||||
@@ -2004,8 +2001,7 @@ pub async fn stripe_webhook(
|
|||||||
EventType::PaymentMethodAttached => {
|
EventType::PaymentMethodAttached => {
|
||||||
if let EventObject::PaymentMethod(payment_method) =
|
if let EventObject::PaymentMethod(payment_method) =
|
||||||
event.data.object
|
event.data.object
|
||||||
{
|
&& let Some(customer_id) =
|
||||||
if let Some(customer_id) =
|
|
||||||
payment_method.customer.map(|x| x.id())
|
payment_method.customer.map(|x| x.id())
|
||||||
{
|
{
|
||||||
let customer = stripe::Customer::retrieve(
|
let customer = stripe::Customer::retrieve(
|
||||||
@@ -2038,7 +2034,6 @@ pub async fn stripe_webhook(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -79,14 +79,13 @@ impl TempUser {
|
|||||||
file_host: &Arc<dyn FileHost + Send + Sync>,
|
file_host: &Arc<dyn FileHost + Send + Sync>,
|
||||||
redis: &RedisPool,
|
redis: &RedisPool,
|
||||||
) -> Result<crate::database::models::DBUserId, AuthenticationError> {
|
) -> Result<crate::database::models::DBUserId, AuthenticationError> {
|
||||||
if let Some(email) = &self.email {
|
if let Some(email) = &self.email
|
||||||
if crate::database::models::DBUser::get_by_email(email, client)
|
&& crate::database::models::DBUser::get_by_email(email, client)
|
||||||
.await?
|
.await?
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
return Err(AuthenticationError::DuplicateUser);
|
return Err(AuthenticationError::DuplicateUser);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let user_id =
|
let user_id =
|
||||||
crate::database::models::generate_user_id(transaction).await?;
|
crate::database::models::generate_user_id(transaction).await?;
|
||||||
@@ -1269,8 +1268,9 @@ pub async fn delete_auth_provider(
|
|||||||
.update_user_id(user.id.into(), None, &mut transaction)
|
.update_user_id(user.id.into(), None, &mut transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if delete_provider.provider != AuthProvider::PayPal {
|
if delete_provider.provider != AuthProvider::PayPal
|
||||||
if let Some(email) = user.email {
|
&& let Some(email) = user.email
|
||||||
|
{
|
||||||
send_email(
|
send_email(
|
||||||
email,
|
email,
|
||||||
"Authentication method removed",
|
"Authentication method removed",
|
||||||
@@ -1282,7 +1282,6 @@ pub async fn delete_auth_provider(
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
crate::database::models::DBUser::clear_caches(
|
crate::database::models::DBUser::clear_caches(
|
||||||
|
|||||||
@@ -189,8 +189,8 @@ pub async fn get_project_meta(
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|x| Some(x.1.id as i32) == row.flame_project_id)
|
.find(|x| Some(x.1.id as i32) == row.flame_project_id)
|
||||||
.map(|x| x.0.clone())
|
.map(|x| x.0.clone())
|
||||||
|
&& let Some(val) = merged.flame_files.remove(&sha1)
|
||||||
{
|
{
|
||||||
if let Some(val) = merged.flame_files.remove(&sha1) {
|
|
||||||
merged.identified.insert(
|
merged.identified.insert(
|
||||||
sha1,
|
sha1,
|
||||||
IdentifiedFile {
|
IdentifiedFile {
|
||||||
@@ -201,7 +201,6 @@ pub async fn get_project_meta(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(merged))
|
Ok(HttpResponse::Ok().json(merged))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -185,8 +185,9 @@ pub async fn edit_pat(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(pat) = pat {
|
if let Some(pat) = pat
|
||||||
if pat.user_id == user.id.into() {
|
&& pat.user_id == user.id.into()
|
||||||
|
{
|
||||||
let mut transaction = pool.begin().await?;
|
let mut transaction = pool.begin().await?;
|
||||||
|
|
||||||
if let Some(scopes) = &info.scopes {
|
if let Some(scopes) = &info.scopes {
|
||||||
@@ -248,7 +249,6 @@ pub async fn edit_pat(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(HttpResponse::NoContent().finish())
|
Ok(HttpResponse::NoContent().finish())
|
||||||
}
|
}
|
||||||
@@ -276,8 +276,9 @@ pub async fn delete_pat(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(pat) = pat {
|
if let Some(pat) = pat
|
||||||
if pat.user_id == user.id.into() {
|
&& pat.user_id == user.id.into()
|
||||||
|
{
|
||||||
let mut transaction = pool.begin().await?;
|
let mut transaction = pool.begin().await?;
|
||||||
database::models::pat_item::DBPersonalAccessToken::remove(
|
database::models::pat_item::DBPersonalAccessToken::remove(
|
||||||
pat.id,
|
pat.id,
|
||||||
@@ -291,7 +292,6 @@ pub async fn delete_pat(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(HttpResponse::NoContent().finish())
|
Ok(HttpResponse::NoContent().finish())
|
||||||
}
|
}
|
||||||
@@ -185,8 +185,9 @@ pub async fn delete(
|
|||||||
|
|
||||||
let session = DBSession::get(info.into_inner().0, &**pool, &redis).await?;
|
let session = DBSession::get(info.into_inner().0, &**pool, &redis).await?;
|
||||||
|
|
||||||
if let Some(session) = session {
|
if let Some(session) = session
|
||||||
if session.user_id == current_user.id.into() {
|
&& session.user_id == current_user.id.into()
|
||||||
|
{
|
||||||
let mut transaction = pool.begin().await?;
|
let mut transaction = pool.begin().await?;
|
||||||
DBSession::remove(session.id, &mut transaction).await?;
|
DBSession::remove(session.id, &mut transaction).await?;
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
@@ -200,7 +201,6 @@ pub async fn delete(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(HttpResponse::NoContent().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -401,8 +401,8 @@ async fn broadcast_to_known_local_friends(
|
|||||||
friend.user_id
|
friend.user_id
|
||||||
};
|
};
|
||||||
|
|
||||||
if friend.accepted {
|
if friend.accepted
|
||||||
if let Some(socket_ids) =
|
&& let Some(socket_ids) =
|
||||||
sockets.sockets_by_user_id.get(&friend_id.into())
|
sockets.sockets_by_user_id.get(&friend_id.into())
|
||||||
{
|
{
|
||||||
for socket_id in socket_ids.iter() {
|
for socket_id in socket_ids.iter() {
|
||||||
@@ -412,7 +412,6 @@ async fn broadcast_to_known_local_friends(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -387,9 +387,10 @@ pub async fn revenue_get(
|
|||||||
.map(|x| (x.to_string(), HashMap::new()))
|
.map(|x| (x.to_string(), HashMap::new()))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
for value in payouts_values {
|
for value in payouts_values {
|
||||||
if let Some(mod_id) = value.mod_id {
|
if let Some(mod_id) = value.mod_id
|
||||||
if let Some(amount) = value.amount_sum {
|
&& let Some(amount) = value.amount_sum
|
||||||
if let Some(interval_start) = value.interval_start {
|
&& let Some(interval_start) = value.interval_start
|
||||||
|
{
|
||||||
let id_string = to_base62(mod_id as u64);
|
let id_string = to_base62(mod_id as u64);
|
||||||
if !hm.contains_key(&id_string) {
|
if !hm.contains_key(&id_string) {
|
||||||
hm.insert(id_string.clone(), HashMap::new());
|
hm.insert(id_string.clone(), HashMap::new());
|
||||||
@@ -399,8 +400,6 @@ pub async fn revenue_get(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(hm))
|
Ok(HttpResponse::Ok().json(hm))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,11 +192,11 @@ pub async fn collection_get(
|
|||||||
.map(|x| x.1)
|
.map(|x| x.1)
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
if let Some(data) = collection_data {
|
if let Some(data) = collection_data
|
||||||
if is_visible_collection(&data, &user_option, false).await? {
|
&& is_visible_collection(&data, &user_option, false).await?
|
||||||
|
{
|
||||||
return Ok(HttpResponse::Ok().json(Collection::from(data)));
|
return Ok(HttpResponse::Ok().json(Collection::from(data)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Err(ApiError::NotFound)
|
Err(ApiError::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -536,11 +536,9 @@ pub async fn create_payout(
|
|||||||
Some(true),
|
Some(true),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
&& let Some(data) = res.items.first()
|
||||||
{
|
{
|
||||||
if let Some(data) = res.items.first() {
|
payout_item.platform_id = Some(data.payout_item_id.clone());
|
||||||
payout_item.platform_id =
|
|
||||||
Some(data.payout_item_id.clone());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -182,11 +182,11 @@ pub async fn project_get(
|
|||||||
.map(|x| x.1)
|
.map(|x| x.1)
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
if let Some(data) = project_data {
|
if let Some(data) = project_data
|
||||||
if is_visible_project(&data.inner, &user_option, &pool, false).await? {
|
&& is_visible_project(&data.inner, &user_option, &pool, false).await?
|
||||||
|
{
|
||||||
return Ok(HttpResponse::Ok().json(Project::from(data)));
|
return Ok(HttpResponse::Ok().json(Project::from(data)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Err(ApiError::NotFound)
|
Err(ApiError::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,8 +403,10 @@ pub async fn project_edit(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.is_searchable() && !project_item.inner.webhook_sent {
|
if status.is_searchable()
|
||||||
if let Ok(webhook_url) = dotenvy::var("PUBLIC_DISCORD_WEBHOOK") {
|
&& !project_item.inner.webhook_sent
|
||||||
|
&& let Ok(webhook_url) = dotenvy::var("PUBLIC_DISCORD_WEBHOOK")
|
||||||
|
{
|
||||||
crate::util::webhook::send_discord_webhook(
|
crate::util::webhook::send_discord_webhook(
|
||||||
project_item.inner.id.into(),
|
project_item.inner.id.into(),
|
||||||
&pool,
|
&pool,
|
||||||
@@ -426,10 +428,10 @@ pub async fn project_edit(
|
|||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if user.role.is_mod() {
|
if user.role.is_mod()
|
||||||
if let Ok(webhook_url) = dotenvy::var("MODERATION_SLACK_WEBHOOK") {
|
&& let Ok(webhook_url) = dotenvy::var("MODERATION_SLACK_WEBHOOK")
|
||||||
|
{
|
||||||
crate::util::webhook::send_slack_webhook(
|
crate::util::webhook::send_slack_webhook(
|
||||||
project_item.inner.id.into(),
|
project_item.inner.id.into(),
|
||||||
&pool,
|
&pool,
|
||||||
@@ -450,7 +452,6 @@ pub async fn project_edit(
|
|||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if team_member.is_none_or(|x| !x.accepted) {
|
if team_member.is_none_or(|x| !x.accepted) {
|
||||||
let notified_members = sqlx::query!(
|
let notified_members = sqlx::query!(
|
||||||
@@ -692,8 +693,9 @@ pub async fn project_edit(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(links) = &new_project.link_urls {
|
if let Some(links) = &new_project.link_urls
|
||||||
if !links.is_empty() {
|
&& !links.is_empty()
|
||||||
|
{
|
||||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||||
return Err(ApiError::CustomAuthentication(
|
return Err(ApiError::CustomAuthentication(
|
||||||
"You do not have the permissions to edit the links of this project!"
|
"You do not have the permissions to edit the links of this project!"
|
||||||
@@ -718,8 +720,7 @@ pub async fn project_edit(
|
|||||||
|
|
||||||
for (platform, url) in links {
|
for (platform, url) in links {
|
||||||
if let Some(url) = url {
|
if let Some(url) = url {
|
||||||
let platform_id =
|
let platform_id = db_models::categories::LinkPlatform::get_id(
|
||||||
db_models::categories::LinkPlatform::get_id(
|
|
||||||
platform,
|
platform,
|
||||||
&mut *transaction,
|
&mut *transaction,
|
||||||
)
|
)
|
||||||
@@ -744,7 +745,6 @@ pub async fn project_edit(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if let Some(moderation_message) = &new_project.moderation_message {
|
if let Some(moderation_message) = &new_project.moderation_message {
|
||||||
if !user.role.is_mod()
|
if !user.role.is_mod()
|
||||||
&& (!project_item.inner.status.is_approved()
|
&& (!project_item.inner.status.is_approved()
|
||||||
@@ -2430,7 +2430,7 @@ pub async fn project_get_organization(
|
|||||||
organization,
|
organization,
|
||||||
team_members,
|
team_members,
|
||||||
);
|
);
|
||||||
return Ok(HttpResponse::Ok().json(organization));
|
Ok(HttpResponse::Ok().json(organization))
|
||||||
} else {
|
} else {
|
||||||
Err(ApiError::NotFound)
|
Err(ApiError::NotFound)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -767,13 +767,14 @@ pub async fn edit_team_member(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(new_permissions) = edit_member.permissions {
|
if let Some(new_permissions) = edit_member.permissions
|
||||||
if !permissions.contains(new_permissions) {
|
&& !permissions.contains(new_permissions)
|
||||||
|
{
|
||||||
return Err(ApiError::InvalidInput(
|
return Err(ApiError::InvalidInput(
|
||||||
"The new permissions have permissions that you don't have".to_string(),
|
"The new permissions have permissions that you don't have"
|
||||||
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if edit_member.organization_permissions.is_some() {
|
if edit_member.organization_permissions.is_some() {
|
||||||
return Err(ApiError::InvalidInput(
|
return Err(ApiError::InvalidInput(
|
||||||
@@ -800,14 +801,13 @@ pub async fn edit_team_member(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(new_permissions) = edit_member.organization_permissions
|
if let Some(new_permissions) = edit_member.organization_permissions
|
||||||
|
&& !organization_permissions.contains(new_permissions)
|
||||||
{
|
{
|
||||||
if !organization_permissions.contains(new_permissions) {
|
|
||||||
return Err(ApiError::InvalidInput(
|
return Err(ApiError::InvalidInput(
|
||||||
"The new organization permissions have permissions that you don't have"
|
"The new organization permissions have permissions that you don't have"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if edit_member.permissions.is_some()
|
if edit_member.permissions.is_some()
|
||||||
&& !organization_permissions.contains(
|
&& !organization_permissions.contains(
|
||||||
@@ -822,14 +822,14 @@ pub async fn edit_team_member(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(payouts_split) = edit_member.payouts_split {
|
if let Some(payouts_split) = edit_member.payouts_split
|
||||||
if payouts_split < Decimal::ZERO || payouts_split > Decimal::from(5000)
|
&& (payouts_split < Decimal::ZERO
|
||||||
|
|| payouts_split > Decimal::from(5000))
|
||||||
{
|
{
|
||||||
return Err(ApiError::InvalidInput(
|
return Err(ApiError::InvalidInput(
|
||||||
"Payouts split must be between 0 and 5000!".to_string(),
|
"Payouts split must be between 0 and 5000!".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
DBTeamMember::edit_team_member(
|
DBTeamMember::edit_team_member(
|
||||||
id,
|
id,
|
||||||
@@ -883,15 +883,15 @@ pub async fn transfer_ownership(
|
|||||||
DBTeam::get_association(id.into(), &**pool).await?;
|
DBTeam::get_association(id.into(), &**pool).await?;
|
||||||
if let Some(TeamAssociationId::Project(pid)) = team_association_id {
|
if let Some(TeamAssociationId::Project(pid)) = team_association_id {
|
||||||
let result = DBProject::get_id(pid, &**pool, &redis).await?;
|
let result = DBProject::get_id(pid, &**pool, &redis).await?;
|
||||||
if let Some(project_item) = result {
|
if let Some(project_item) = result
|
||||||
if project_item.inner.organization_id.is_some() {
|
&& project_item.inner.organization_id.is_some()
|
||||||
|
{
|
||||||
return Err(ApiError::InvalidInput(
|
return Err(ApiError::InvalidInput(
|
||||||
"You cannot transfer ownership of a project team that is owend by an organization"
|
"You cannot transfer ownership of a project team that is owend by an organization"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !current_user.role.is_admin() {
|
if !current_user.role.is_admin() {
|
||||||
let member = DBTeamMember::get_from_user_id(
|
let member = DBTeamMember::get_from_user_id(
|
||||||
|
|||||||
@@ -289,8 +289,9 @@ pub async fn thread_get(
|
|||||||
.await?
|
.await?
|
||||||
.1;
|
.1;
|
||||||
|
|
||||||
if let Some(mut data) = thread_data {
|
if let Some(mut data) = thread_data
|
||||||
if is_authorized_thread(&data, &user, &pool).await? {
|
&& is_authorized_thread(&data, &user, &pool).await?
|
||||||
|
{
|
||||||
let authors = &mut data.members;
|
let authors = &mut data.members;
|
||||||
|
|
||||||
authors.append(
|
authors.append(
|
||||||
@@ -307,18 +308,14 @@ pub async fn thread_get(
|
|||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let users: Vec<User> = database::models::DBUser::get_many_ids(
|
let users: Vec<User> =
|
||||||
authors, &**pool, &redis,
|
database::models::DBUser::get_many_ids(authors, &**pool, &redis)
|
||||||
)
|
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(From::from)
|
.map(From::from)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
return Ok(
|
return Ok(HttpResponse::Ok().json(Thread::from(data, users, &user)));
|
||||||
HttpResponse::Ok().json(Thread::from(data, users, &user))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(ApiError::NotFound)
|
Err(ApiError::NotFound)
|
||||||
}
|
}
|
||||||
@@ -454,8 +451,8 @@ pub async fn thread_send_message(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(project) = project {
|
if let Some(project) = project
|
||||||
if project.inner.status != ProjectStatus::Processing
|
&& project.inner.status != ProjectStatus::Processing
|
||||||
&& user.role.is_mod()
|
&& user.role.is_mod()
|
||||||
{
|
{
|
||||||
let members =
|
let members =
|
||||||
@@ -481,7 +478,6 @@ pub async fn thread_send_message(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else if let Some(report_id) = thread.report_id {
|
} else if let Some(report_id) = thread.report_id {
|
||||||
let report = database::models::report_item::DBReport::get(
|
let report = database::models::report_item::DBReport::get(
|
||||||
report_id, &**pool,
|
report_id, &**pool,
|
||||||
|
|||||||
@@ -522,11 +522,11 @@ async fn version_create_inner(
|
|||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(project_status) = project_status {
|
if let Some(project_status) = project_status
|
||||||
if project_status.status == ProjectStatus::Processing.as_str() {
|
&& project_status.status == ProjectStatus::Processing.as_str()
|
||||||
|
{
|
||||||
moderation_queue.projects.insert(project_id.into());
|
moderation_queue.projects.insert(project_id.into());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(response))
|
Ok(HttpResponse::Ok().json(response))
|
||||||
}
|
}
|
||||||
@@ -871,8 +871,8 @@ pub async fn upload_file(
|
|||||||
ref format,
|
ref format,
|
||||||
ref files,
|
ref files,
|
||||||
} = validation_result
|
} = validation_result
|
||||||
|
&& dependencies.is_empty()
|
||||||
{
|
{
|
||||||
if dependencies.is_empty() {
|
|
||||||
let hashes: Vec<Vec<u8>> = format
|
let hashes: Vec<Vec<u8>> = format
|
||||||
.files
|
.files
|
||||||
.iter()
|
.iter()
|
||||||
@@ -933,7 +933,6 @@ pub async fn upload_file(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let data = data.freeze();
|
let data = data.freeze();
|
||||||
let primary = (validation_result.is_passed()
|
let primary = (validation_result.is_passed()
|
||||||
@@ -974,11 +973,11 @@ pub async fn upload_file(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let ValidationResult::Warning(msg) = validation_result {
|
if let ValidationResult::Warning(msg) = validation_result
|
||||||
if primary {
|
&& primary
|
||||||
|
{
|
||||||
return Err(CreateError::InvalidInput(msg.to_string()));
|
return Err(CreateError::InvalidInput(msg.to_string()));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let url = format!("{cdn_url}/{file_path_encode}");
|
let url = format!("{cdn_url}/{file_path_encode}");
|
||||||
|
|
||||||
|
|||||||
@@ -148,8 +148,7 @@ pub async fn get_update_from_hash(
|
|||||||
&redis,
|
&redis,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
{
|
&& let Some(project) = database::models::DBProject::get_id(
|
||||||
if let Some(project) = database::models::DBProject::get_id(
|
|
||||||
file.project_id,
|
file.project_id,
|
||||||
&**pool,
|
&**pool,
|
||||||
&redis,
|
&redis,
|
||||||
@@ -175,14 +174,10 @@ pub async fn get_update_from_hash(
|
|||||||
}
|
}
|
||||||
if let Some(loader_fields) = &update_data.loader_fields {
|
if let Some(loader_fields) = &update_data.loader_fields {
|
||||||
for (key, values) in loader_fields {
|
for (key, values) in loader_fields {
|
||||||
bool &= if let Some(x_vf) = x
|
bool &= if let Some(x_vf) =
|
||||||
.version_fields
|
x.version_fields.iter().find(|y| y.field_name == *key)
|
||||||
.iter()
|
|
||||||
.find(|y| y.field_name == *key)
|
|
||||||
{
|
{
|
||||||
values
|
values.iter().any(|v| x_vf.value.contains_json_value(v))
|
||||||
.iter()
|
|
||||||
.any(|v| x_vf.value.contains_json_value(v))
|
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
};
|
};
|
||||||
@@ -193,20 +188,15 @@ pub async fn get_update_from_hash(
|
|||||||
.sorted();
|
.sorted();
|
||||||
|
|
||||||
if let Some(first) = versions.next_back() {
|
if let Some(first) = versions.next_back() {
|
||||||
if !is_visible_version(
|
if !is_visible_version(&first.inner, &user_option, &pool, &redis)
|
||||||
&first.inner,
|
|
||||||
&user_option,
|
|
||||||
&pool,
|
|
||||||
&redis,
|
|
||||||
)
|
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
return Err(ApiError::NotFound);
|
return Err(ApiError::NotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(HttpResponse::Ok()
|
return Ok(
|
||||||
.json(models::projects::Version::from(first)));
|
HttpResponse::Ok().json(models::projects::Version::from(first))
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(ApiError::NotFound)
|
Err(ApiError::NotFound)
|
||||||
@@ -398,15 +388,14 @@ pub async fn update_files(
|
|||||||
if let Some(version) = versions
|
if let Some(version) = versions
|
||||||
.iter()
|
.iter()
|
||||||
.find(|x| x.inner.project_id == file.project_id)
|
.find(|x| x.inner.project_id == file.project_id)
|
||||||
|
&& let Some(hash) = file.hashes.get(&algorithm)
|
||||||
{
|
{
|
||||||
if let Some(hash) = file.hashes.get(&algorithm) {
|
|
||||||
response.insert(
|
response.insert(
|
||||||
hash.clone(),
|
hash.clone(),
|
||||||
models::projects::Version::from(version.clone()),
|
models::projects::Version::from(version.clone()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(response))
|
Ok(HttpResponse::Ok().json(response))
|
||||||
}
|
}
|
||||||
@@ -484,8 +473,8 @@ pub async fn update_individual_files(
|
|||||||
|
|
||||||
for project in projects {
|
for project in projects {
|
||||||
for file in files.iter().filter(|x| x.project_id == project.inner.id) {
|
for file in files.iter().filter(|x| x.project_id == project.inner.id) {
|
||||||
if let Some(hash) = file.hashes.get(&algorithm) {
|
if let Some(hash) = file.hashes.get(&algorithm)
|
||||||
if let Some(query_file) =
|
&& let Some(query_file) =
|
||||||
update_data.hashes.iter().find(|x| &x.hash == hash)
|
update_data.hashes.iter().find(|x| &x.hash == hash)
|
||||||
{
|
{
|
||||||
let version = all_versions
|
let version = all_versions
|
||||||
@@ -494,23 +483,17 @@ pub async fn update_individual_files(
|
|||||||
.filter(|x| {
|
.filter(|x| {
|
||||||
let mut bool = true;
|
let mut bool = true;
|
||||||
|
|
||||||
if let Some(version_types) =
|
if let Some(version_types) = &query_file.version_types {
|
||||||
&query_file.version_types
|
bool &= version_types
|
||||||
{
|
.iter()
|
||||||
bool &= version_types.iter().any(|y| {
|
.any(|y| y.as_str() == x.inner.version_type);
|
||||||
y.as_str() == x.inner.version_type
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if let Some(loaders) = &query_file.loaders {
|
if let Some(loaders) = &query_file.loaders {
|
||||||
bool &= x
|
bool &=
|
||||||
.loaders
|
x.loaders.iter().any(|y| loaders.contains(y));
|
||||||
.iter()
|
|
||||||
.any(|y| loaders.contains(y));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(loader_fields) =
|
if let Some(loader_fields) = &query_file.loader_fields {
|
||||||
&query_file.loader_fields
|
|
||||||
{
|
|
||||||
for (key, values) in loader_fields {
|
for (key, values) in loader_fields {
|
||||||
bool &= if let Some(x_vf) = x
|
bool &= if let Some(x_vf) = x
|
||||||
.version_fields
|
.version_fields
|
||||||
@@ -530,8 +513,8 @@ pub async fn update_individual_files(
|
|||||||
.sorted()
|
.sorted()
|
||||||
.next_back();
|
.next_back();
|
||||||
|
|
||||||
if let Some(version) = version {
|
if let Some(version) = version
|
||||||
if is_visible_version(
|
&& is_visible_version(
|
||||||
&version.inner,
|
&version.inner,
|
||||||
&user_option,
|
&user_option,
|
||||||
&pool,
|
&pool,
|
||||||
@@ -541,16 +524,12 @@ pub async fn update_individual_files(
|
|||||||
{
|
{
|
||||||
response.insert(
|
response.insert(
|
||||||
hash.clone(),
|
hash.clone(),
|
||||||
models::projects::Version::from(
|
models::projects::Version::from(version.clone()),
|
||||||
version.clone(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(response))
|
Ok(HttpResponse::Ok().json(response))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,15 +106,14 @@ pub async fn version_project_get_helper(
|
|||||||
|| x.inner.version_number == id.1
|
|| x.inner.version_number == id.1
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(version) = version {
|
if let Some(version) = version
|
||||||
if is_visible_version(&version.inner, &user_option, &pool, &redis)
|
&& is_visible_version(&version.inner, &user_option, &pool, &redis)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
return Ok(HttpResponse::Ok()
|
return Ok(HttpResponse::Ok()
|
||||||
.json(models::projects::Version::from(version)));
|
.json(models::projects::Version::from(version)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Err(ApiError::NotFound)
|
Err(ApiError::NotFound)
|
||||||
}
|
}
|
||||||
@@ -190,13 +189,13 @@ pub async fn version_get_helper(
|
|||||||
.map(|x| x.1)
|
.map(|x| x.1)
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
if let Some(data) = version_data {
|
if let Some(data) = version_data
|
||||||
if is_visible_version(&data.inner, &user_option, &pool, &redis).await? {
|
&& is_visible_version(&data.inner, &user_option, &pool, &redis).await?
|
||||||
|
{
|
||||||
return Ok(
|
return Ok(
|
||||||
HttpResponse::Ok().json(models::projects::Version::from(data))
|
HttpResponse::Ok().json(models::projects::Version::from(data))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Err(ApiError::NotFound)
|
Err(ApiError::NotFound)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,13 @@ pub async fn get_user_status(
|
|||||||
return Some(friend_status);
|
return Some(friend_status);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(mut conn) = redis.pool.get().await {
|
if let Ok(mut conn) = redis.pool.get().await
|
||||||
if let Ok(mut statuses) =
|
&& let Ok(mut statuses) =
|
||||||
conn.sscan::<_, String>(get_field_name(user)).await
|
conn.sscan::<_, String>(get_field_name(user)).await
|
||||||
|
&& let Some(status_json) = statuses.next_item().await
|
||||||
{
|
{
|
||||||
if let Some(status_json) = statuses.next_item().await {
|
|
||||||
return serde_json::from_str::<UserStatus>(&status_json).ok();
|
return serde_json::from_str::<UserStatus>(&status_json).ok();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,13 +138,12 @@ fn process_image(
|
|||||||
let (orig_width, orig_height) = img.dimensions();
|
let (orig_width, orig_height) = img.dimensions();
|
||||||
let aspect_ratio = orig_width as f32 / orig_height as f32;
|
let aspect_ratio = orig_width as f32 / orig_height as f32;
|
||||||
|
|
||||||
if let Some(target_width) = target_width {
|
if let Some(target_width) = target_width
|
||||||
if img.width() > target_width {
|
&& img.width() > target_width
|
||||||
let new_height =
|
{
|
||||||
(target_width as f32 / aspect_ratio).round() as u32;
|
let new_height = (target_width as f32 / aspect_ratio).round() as u32;
|
||||||
img = img.resize(target_width, new_height, FilterType::Lanczos3);
|
img = img.resize(target_width, new_height, FilterType::Lanczos3);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(min_aspect_ratio) = min_aspect_ratio {
|
if let Some(min_aspect_ratio) = min_aspect_ratio {
|
||||||
// Crop if necessary
|
// Crop if necessary
|
||||||
|
|||||||
@@ -133,13 +133,12 @@ pub async fn rate_limit_middleware(
|
|||||||
.expect("Rate limiter not configured properly")
|
.expect("Rate limiter not configured properly")
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
if let Some(key) = req.headers().get("x-ratelimit-key") {
|
if let Some(key) = req.headers().get("x-ratelimit-key")
|
||||||
if key.to_str().ok()
|
&& key.to_str().ok()
|
||||||
== dotenvy::var("RATE_LIMIT_IGNORE_KEY").ok().as_deref()
|
== dotenvy::var("RATE_LIMIT_IGNORE_KEY").ok().as_deref()
|
||||||
{
|
{
|
||||||
return Ok(next.call(req).await?.map_into_left_body());
|
return Ok(next.call(req).await?.map_into_left_body());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let conn_info = req.connection_info().clone();
|
let conn_info = req.connection_info().clone();
|
||||||
let ip = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) {
|
let ip = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) {
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ pub fn validation_errors_to_string(
|
|||||||
|
|
||||||
let key_option = map.keys().next();
|
let key_option = map.keys().next();
|
||||||
|
|
||||||
if let Some(field) = key_option {
|
if let Some(field) = key_option
|
||||||
if let Some(error) = map.get(field) {
|
&& let Some(error) = map.get(field)
|
||||||
|
{
|
||||||
return match error {
|
return match error {
|
||||||
ValidationErrorsKind::Struct(errors) => {
|
ValidationErrorsKind::Struct(errors) => {
|
||||||
validation_errors_to_string(
|
validation_errors_to_string(
|
||||||
@@ -54,7 +55,8 @@ pub fn validation_errors_to_string(
|
|||||||
&mut output,
|
&mut output,
|
||||||
"Field {field} failed validation with error: {}",
|
"Field {field} failed validation with error: {}",
|
||||||
error.code
|
error.code
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +64,6 @@ pub fn validation_errors_to_string(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,8 +238,9 @@ pub async fn send_slack_webhook(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(icon_url) = metadata.project_icon_url {
|
if let Some(icon_url) = metadata.project_icon_url
|
||||||
if let Some(project_block) = project_block.as_object_mut() {
|
&& let Some(project_block) = project_block.as_object_mut()
|
||||||
|
{
|
||||||
project_block.insert(
|
project_block.insert(
|
||||||
"accessory".to_string(),
|
"accessory".to_string(),
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
@@ -249,7 +250,6 @@ pub async fn send_slack_webhook(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
blocks.push(project_block);
|
blocks.push(project_block);
|
||||||
|
|
||||||
|
|||||||
@@ -222,11 +222,11 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
|||||||
resp.status().as_u16()
|
resp.status().as_u16()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if resp.status() == StatusCode::OK {
|
if resp.status() == StatusCode::OK
|
||||||
if let Some(failure_json_check) = &self.failure_json_check {
|
&& let Some(failure_json_check) = &self.failure_json_check
|
||||||
|
{
|
||||||
failure_json_check(&test::read_body_json(resp).await);
|
failure_json_check(&test::read_body_json(resp).await);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Failure test- logged in on a non-team user
|
// Failure test- logged in on a non-team user
|
||||||
let resp = req_gen(PermissionsTestContext {
|
let resp = req_gen(PermissionsTestContext {
|
||||||
@@ -246,11 +246,11 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
|||||||
resp.status().as_u16()
|
resp.status().as_u16()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if resp.status() == StatusCode::OK {
|
if resp.status() == StatusCode::OK
|
||||||
if let Some(failure_json_check) = &self.failure_json_check {
|
&& let Some(failure_json_check) = &self.failure_json_check
|
||||||
|
{
|
||||||
failure_json_check(&test::read_body_json(resp).await);
|
failure_json_check(&test::read_body_json(resp).await);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Failure test- logged in with EVERY non-relevant permission
|
// Failure test- logged in with EVERY non-relevant permission
|
||||||
let resp: ServiceResponse = req_gen(PermissionsTestContext {
|
let resp: ServiceResponse = req_gen(PermissionsTestContext {
|
||||||
@@ -270,11 +270,11 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
|||||||
resp.status().as_u16()
|
resp.status().as_u16()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if resp.status() == StatusCode::OK {
|
if resp.status() == StatusCode::OK
|
||||||
if let Some(failure_json_check) = &self.failure_json_check {
|
&& let Some(failure_json_check) = &self.failure_json_check
|
||||||
|
{
|
||||||
failure_json_check(&test::read_body_json(resp).await);
|
failure_json_check(&test::read_body_json(resp).await);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Patch user's permissions to success permissions
|
// Patch user's permissions to success permissions
|
||||||
modify_user_team_permissions(
|
modify_user_team_permissions(
|
||||||
@@ -300,11 +300,11 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
|||||||
resp.status().as_u16()
|
resp.status().as_u16()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if resp.status() == StatusCode::OK {
|
if resp.status() == StatusCode::OK
|
||||||
if let Some(success_json_check) = &self.success_json_check {
|
&& let Some(success_json_check) = &self.success_json_check
|
||||||
|
{
|
||||||
success_json_check(&test::read_body_json(resp).await);
|
success_json_check(&test::read_body_json(resp).await);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If the remove_user flag is set, remove the user from the project
|
// If the remove_user flag is set, remove the user from the project
|
||||||
// Relevant for existing projects/users
|
// Relevant for existing projects/users
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
allow-dbg-in-tests = true
|
allow-dbg-in-tests = true
|
||||||
msrv = "1.88.0"
|
msrv = "1.89.0"
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
"blog:fix": "turbo run fix --filter=@modrinth/blog",
|
"blog:fix": "turbo run fix --filter=@modrinth/blog",
|
||||||
"pages:build": "NITRO_PRESET=cloudflare-pages pnpm --filter frontend run build",
|
"pages:build": "NITRO_PRESET=cloudflare-pages pnpm --filter frontend run build",
|
||||||
"moderation:fix": "turbo run fix --filter=@modrinth/moderation",
|
"moderation:fix": "turbo run fix --filter=@modrinth/moderation",
|
||||||
"moderation:intl:extract": "pnpm run --filter=@modrinth/moderation intl:extract",
|
|
||||||
"build": "turbo run build --continue",
|
"build": "turbo run build --continue",
|
||||||
"lint": "turbo run lint --continue",
|
"lint": "turbo run lint --continue",
|
||||||
"test": "turbo run test --continue",
|
"test": "turbo run test --continue",
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ pub async fn parse_command(
|
|||||||
// We assume anything else is a filepath to an .mrpack file
|
// We assume anything else is a filepath to an .mrpack file
|
||||||
let path = PathBuf::from(command_string);
|
let path = PathBuf::from(command_string);
|
||||||
let path = io::canonicalize(path)?;
|
let path = io::canonicalize(path)?;
|
||||||
if let Some(ext) = path.extension() {
|
if let Some(ext) = path.extension()
|
||||||
if ext == "mrpack" {
|
&& ext == "mrpack"
|
||||||
|
{
|
||||||
return Ok(CommandPayload::RunMRPack { path });
|
return Ok(CommandPayload::RunMRPack { path });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
emit_warning(&format!(
|
emit_warning(&format!(
|
||||||
"Invalid command, unrecognized filetype: {}",
|
"Invalid command, unrecognized filetype: {}",
|
||||||
path.display()
|
path.display()
|
||||||
|
|||||||
@@ -106,15 +106,15 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
// removes the old installation of java
|
// removes the old installation of java
|
||||||
if let Some(file) = archive.file_names().next() {
|
if let Some(file) = archive.file_names().next()
|
||||||
if let Some(dir) = file.split('/').next() {
|
&& let Some(dir) = file.split('/').next()
|
||||||
|
{
|
||||||
let path = path.join(dir);
|
let path = path.join(dir);
|
||||||
|
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
io::remove_dir_all(path).await?;
|
io::remove_dir_all(path).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
emit_loading(&loading_bar, 0.0, Some("Extracting java"))?;
|
emit_loading(&loading_bar, 0.0, Some("Extracting java"))?;
|
||||||
archive.extract(&path).map_err(|_| {
|
archive.extract(&path).map_err(|_| {
|
||||||
|
|||||||
@@ -54,13 +54,13 @@ pub async fn remove_user(uuid: uuid::Uuid) -> crate::Result<()> {
|
|||||||
if let Some((uuid, user)) = users.remove(&uuid) {
|
if let Some((uuid, user)) = users.remove(&uuid) {
|
||||||
Credentials::remove(uuid, &state.pool).await?;
|
Credentials::remove(uuid, &state.pool).await?;
|
||||||
|
|
||||||
if user.active {
|
if user.active
|
||||||
if let Some((_, mut user)) = users.into_iter().next() {
|
&& let Some((_, mut user)) = users.into_iter().next()
|
||||||
|
{
|
||||||
user.active = true;
|
user.active = true;
|
||||||
user.upsert(&state.pool).await?;
|
user.upsert(&state.pool).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,15 +221,15 @@ async fn import_atlauncher_unmanaged(
|
|||||||
.unwrap_or_else(|| backup_name.to_string());
|
.unwrap_or_else(|| backup_name.to_string());
|
||||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||||
|
|
||||||
if let Some(ref project_id) = description.project_id {
|
if let Some(ref project_id) = description.project_id
|
||||||
if let Some(ref version_id) = description.version_id {
|
&& let Some(ref version_id) = description.version_id
|
||||||
|
{
|
||||||
prof.linked_data = Some(LinkedData {
|
prof.linked_data = Some(LinkedData {
|
||||||
project_id: project_id.clone(),
|
project_id: project_id.clone(),
|
||||||
version_id: version_id.clone(),
|
version_id: version_id.clone(),
|
||||||
locked: true,
|
locked: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
prof.icon_path = description
|
prof.icon_path = description
|
||||||
.icon
|
.icon
|
||||||
|
|||||||
@@ -383,8 +383,9 @@ pub async fn set_profile_information(
|
|||||||
.unwrap_or_else(|| backup_name.to_string());
|
.unwrap_or_else(|| backup_name.to_string());
|
||||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||||
|
|
||||||
if let Some(ref project_id) = description.project_id {
|
if let Some(ref project_id) = description.project_id
|
||||||
if let Some(ref version_id) = description.version_id {
|
&& let Some(ref version_id) = description.version_id
|
||||||
|
{
|
||||||
prof.linked_data = Some(LinkedData {
|
prof.linked_data = Some(LinkedData {
|
||||||
project_id: project_id.clone(),
|
project_id: project_id.clone(),
|
||||||
version_id: version_id.clone(),
|
version_id: version_id.clone(),
|
||||||
@@ -395,7 +396,6 @@ pub async fn set_profile_information(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
prof.icon_path = description
|
prof.icon_path = description
|
||||||
.icon
|
.icon
|
||||||
|
|||||||
@@ -149,14 +149,13 @@ pub async fn install_zipped_mrpack_files(
|
|||||||
let profile_path = profile_path.clone();
|
let profile_path = profile_path.clone();
|
||||||
async move {
|
async move {
|
||||||
//TODO: Future update: prompt user for optional files in a modpack
|
//TODO: Future update: prompt user for optional files in a modpack
|
||||||
if let Some(env) = project.env {
|
if let Some(env) = project.env
|
||||||
if env
|
&& env
|
||||||
.get(&EnvType::Client)
|
.get(&EnvType::Client)
|
||||||
.is_some_and(|x| x == &SideType::Unsupported)
|
.is_some_and(|x| x == &SideType::Unsupported)
|
||||||
{
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let file = fetch_mirrors(
|
let file = fetch_mirrors(
|
||||||
&project
|
&project
|
||||||
@@ -375,15 +374,15 @@ pub async fn remove_all_related_files(
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
if let Some(metadata) = &project.metadata {
|
if let Some(metadata) = &project.metadata
|
||||||
if to_remove.contains(&metadata.project_id) {
|
&& to_remove.contains(&metadata.project_id)
|
||||||
|
{
|
||||||
let path = profile_full_path.join(file_path);
|
let path = profile_full_path.join(file_path);
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
io::remove_file(&path).await?;
|
io::remove_file(&path).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate over all Modrinth project file paths in the json, and remove them
|
// Iterate over all Modrinth project file paths in the json, and remove them
|
||||||
// (There should be few, but this removes any files the .mrpack intended as Modrinth projects but were unrecognized)
|
// (There should be few, but this removes any files the .mrpack intended as Modrinth projects but were unrecognized)
|
||||||
|
|||||||
@@ -337,8 +337,8 @@ pub async fn update_project(
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.remove(project_path)
|
.remove(project_path)
|
||||||
|
&& let Some(update_version) = &file.update_version_id
|
||||||
{
|
{
|
||||||
if let Some(update_version) = &file.update_version_id {
|
|
||||||
let path = Profile::add_project_version(
|
let path = Profile::add_project_version(
|
||||||
profile_path,
|
profile_path,
|
||||||
update_version,
|
update_version,
|
||||||
@@ -353,13 +353,11 @@ pub async fn update_project(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !skip_send_event.unwrap_or(false) {
|
if !skip_send_event.unwrap_or(false) {
|
||||||
emit_profile(profile_path, ProfilePayloadType::Edited)
|
emit_profile(profile_path, ProfilePayloadType::Edited).await?;
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(path);
|
return Ok(path);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Err(crate::ErrorKind::InputError(
|
Err(crate::ErrorKind::InputError(
|
||||||
"This project cannot be updated!".to_string(),
|
"This project cannot be updated!".to_string(),
|
||||||
@@ -479,11 +477,11 @@ pub async fn export_mrpack(
|
|||||||
let included_export_candidates = included_export_candidates
|
let included_export_candidates = included_export_candidates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|x| {
|
.filter(|x| {
|
||||||
if let Some(f) = PathBuf::from(x).file_name() {
|
if let Some(f) = PathBuf::from(x).file_name()
|
||||||
if f.to_string_lossy().starts_with(".DS_Store") {
|
&& f.to_string_lossy().starts_with(".DS_Store")
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ pub enum LoadingBarType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
|
#[cfg(feature = "tauri")]
|
||||||
pub struct LoadingPayload {
|
pub struct LoadingPayload {
|
||||||
pub event: LoadingBarType,
|
pub event: LoadingBarType,
|
||||||
pub loader_uuid: Uuid,
|
pub loader_uuid: Uuid,
|
||||||
@@ -192,11 +193,7 @@ pub struct LoadingPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
pub struct OfflinePayload {
|
#[cfg(feature = "tauri")]
|
||||||
pub offline: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
|
||||||
pub struct WarningPayload {
|
pub struct WarningPayload {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
@@ -220,12 +217,14 @@ pub enum CommandPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
|
#[cfg(feature = "tauri")]
|
||||||
pub struct ProcessPayload {
|
pub struct ProcessPayload {
|
||||||
pub profile_path_id: String,
|
pub profile_path_id: String,
|
||||||
pub uuid: Uuid,
|
pub uuid: Uuid,
|
||||||
pub event: ProcessPayloadType,
|
pub event: ProcessPayloadType,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug)]
|
#[derive(Serialize, Clone, Debug)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum ProcessPayloadType {
|
pub enum ProcessPayloadType {
|
||||||
@@ -234,11 +233,13 @@ pub enum ProcessPayloadType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
|
#[cfg(feature = "tauri")]
|
||||||
pub struct ProfilePayload {
|
pub struct ProfilePayload {
|
||||||
pub profile_path_id: String,
|
pub profile_path_id: String,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub event: ProfilePayloadType,
|
pub event: ProfilePayloadType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
#[serde(tag = "event", rename_all = "snake_case")]
|
#[serde(tag = "event", rename_all = "snake_case")]
|
||||||
pub enum ProfilePayloadType {
|
pub enum ProfilePayloadType {
|
||||||
@@ -257,6 +258,16 @@ pub enum ProfilePayloadType {
|
|||||||
Removed,
|
Removed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[serde(tag = "event")]
|
||||||
|
pub enum FriendPayload {
|
||||||
|
FriendRequest { from: UserId },
|
||||||
|
UserOffline { id: UserId },
|
||||||
|
StatusUpdate { user_status: UserStatus },
|
||||||
|
StatusSync,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum EventError {
|
pub enum EventError {
|
||||||
#[error("Event state was not properly initialized")]
|
#[error("Event state was not properly initialized")]
|
||||||
@@ -269,13 +280,3 @@ pub enum EventError {
|
|||||||
#[error("Tauri error: {0}")]
|
#[error("Tauri error: {0}")]
|
||||||
TauriError(#[from] tauri::Error),
|
TauriError(#[from] tauri::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
#[serde(tag = "event")]
|
|
||||||
pub enum FriendPayload {
|
|
||||||
FriendRequest { from: UserId },
|
|
||||||
UserOffline { id: UserId },
|
|
||||||
StatusUpdate { user_status: UserStatus },
|
|
||||||
StatusSync,
|
|
||||||
}
|
|
||||||
@@ -32,16 +32,16 @@ pub fn get_class_paths(
|
|||||||
let mut cps = libraries
|
let mut cps = libraries
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|library| {
|
.filter_map(|library| {
|
||||||
if let Some(rules) = &library.rules {
|
if let Some(rules) = &library.rules
|
||||||
if !parse_rules(
|
&& !parse_rules(
|
||||||
rules,
|
rules,
|
||||||
java_arch,
|
java_arch,
|
||||||
&QuickPlayType::None,
|
&QuickPlayType::None,
|
||||||
minecraft_updated,
|
minecraft_updated,
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !library.include_in_classpath {
|
if !library.include_in_classpath {
|
||||||
return None;
|
return None;
|
||||||
@@ -504,12 +504,12 @@ pub async fn get_processor_main_class(
|
|||||||
let mut line = line.map_err(IOError::from)?;
|
let mut line = line.map_err(IOError::from)?;
|
||||||
line.retain(|c| !c.is_whitespace());
|
line.retain(|c| !c.is_whitespace());
|
||||||
|
|
||||||
if line.starts_with("Main-Class:") {
|
if line.starts_with("Main-Class:")
|
||||||
if let Some(class) = line.split(':').nth(1) {
|
&& let Some(class) = line.split(':').nth(1)
|
||||||
|
{
|
||||||
return Ok(Some(class.to_string()));
|
return Ok(Some(class.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<Option<String>, crate::Error>(None)
|
Ok::<Option<String>, crate::Error>(None)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -290,12 +290,11 @@ pub async fn download_libraries(
|
|||||||
loading_try_for_each_concurrent(
|
loading_try_for_each_concurrent(
|
||||||
stream::iter(libraries.iter())
|
stream::iter(libraries.iter())
|
||||||
.map(Ok::<&Library, crate::Error>), None, loading_bar,loading_amount,num_files, None,|library| async move {
|
.map(Ok::<&Library, crate::Error>), None, loading_bar,loading_amount,num_files, None,|library| async move {
|
||||||
if let Some(rules) = &library.rules {
|
if let Some(rules) = &library.rules
|
||||||
if !parse_rules(rules, java_arch, &QuickPlayType::None, minecraft_updated) {
|
&& !parse_rules(rules, java_arch, &QuickPlayType::None, minecraft_updated) {
|
||||||
tracing::trace!("Skipped library {}", &library.name);
|
tracing::trace!("Skipped library {}", &library.name);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !library.downloadable {
|
if !library.downloadable {
|
||||||
tracing::trace!("Skipped non-downloadable library {}", &library.name);
|
tracing::trace!("Skipped non-downloadable library {}", &library.name);
|
||||||
@@ -311,15 +310,14 @@ pub async fn download_libraries(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(d::minecraft::LibraryDownloads { artifact: Some(ref artifact), ..}) = library.downloads {
|
if let Some(d::minecraft::LibraryDownloads { artifact: Some(ref artifact), ..}) = library.downloads
|
||||||
if !artifact.url.is_empty(){
|
&& !artifact.url.is_empty(){
|
||||||
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore, &st.pool)
|
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore, &st.pool)
|
||||||
.await?;
|
.await?;
|
||||||
write(&path, &bytes, &st.io_semaphore).await?;
|
write(&path, &bytes, &st.io_semaphore).await?;
|
||||||
tracing::trace!("Fetched library {} to path {:?}", &library.name, &path);
|
tracing::trace!("Fetched library {} to path {:?}", &library.name, &path);
|
||||||
return Ok::<_, crate::Error>(());
|
return Ok::<_, crate::Error>(());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let url = [
|
let url = [
|
||||||
library
|
library
|
||||||
|
|||||||
@@ -341,11 +341,11 @@ pub async fn install_minecraft(
|
|||||||
|
|
||||||
// Forge processors (90-100)
|
// Forge processors (90-100)
|
||||||
for (index, processor) in processors.iter().enumerate() {
|
for (index, processor) in processors.iter().enumerate() {
|
||||||
if let Some(sides) = &processor.sides {
|
if let Some(sides) = &processor.sides
|
||||||
if !sides.contains(&String::from("client")) {
|
&& !sides.contains(&String::from("client"))
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let cp = {
|
let cp = {
|
||||||
let mut cp = processor.classpath.clone();
|
let mut cp = processor.classpath.clone();
|
||||||
|
|||||||
@@ -385,11 +385,11 @@ impl DirectoryInfo {
|
|||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let Some(disk_usage) = get_disk_usage(&move_dir)? {
|
if let Some(disk_usage) = get_disk_usage(&move_dir)?
|
||||||
if total_size > disk_usage {
|
&& total_size > disk_usage
|
||||||
|
{
|
||||||
return Err(crate::ErrorKind::DirectoryMoveError(format!("Not enough space to move directory to {}: only {} bytes available", app_dir.display(), disk_usage)).into());
|
return Err(crate::ErrorKind::DirectoryMoveError(format!("Not enough space to move directory to {}: only {} bytes available", app_dir.display(), disk_usage)).into());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let loader_bar_id = Arc::new(&loader_bar_id);
|
let loader_bar_id = Arc::new(&loader_bar_id);
|
||||||
futures::future::try_join_all(paths.iter().map(|x| {
|
futures::future::try_join_all(paths.iter().map(|x| {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use ariadne::networking::message::{
|
|||||||
ClientToServerMessage, ServerToClientMessage,
|
ClientToServerMessage, ServerToClientMessage,
|
||||||
};
|
};
|
||||||
use ariadne::users::UserStatus;
|
use ariadne::users::UserStatus;
|
||||||
use async_tungstenite::WebSocketStream;
|
use async_tungstenite::WebSocketSender;
|
||||||
use async_tungstenite::tokio::{ConnectStream, connect_async};
|
use async_tungstenite::tokio::{ConnectStream, connect_async};
|
||||||
use async_tungstenite::tungstenite::Message;
|
use async_tungstenite::tungstenite::Message;
|
||||||
use async_tungstenite::tungstenite::client::IntoClientRequest;
|
use async_tungstenite::tungstenite::client::IntoClientRequest;
|
||||||
@@ -17,7 +17,6 @@ use bytes::Bytes;
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use either::Either;
|
use either::Either;
|
||||||
use futures::stream::SplitSink;
|
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use reqwest::header::HeaderValue;
|
use reqwest::header::HeaderValue;
|
||||||
@@ -32,7 +31,7 @@ use tokio::sync::{Mutex, RwLock};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub(super) type WriteSocket =
|
pub(super) type WriteSocket =
|
||||||
Arc<RwLock<Option<SplitSink<WebSocketStream<ConnectStream>, Message>>>>;
|
Arc<RwLock<Option<WebSocketSender<ConnectStream>>>>;
|
||||||
pub(super) type TunnelSockets = Arc<DashMap<Uuid, Arc<InternalTunnelSocket>>>;
|
pub(super) type TunnelSockets = Arc<DashMap<Uuid, Arc<InternalTunnelSocket>>>;
|
||||||
|
|
||||||
pub struct FriendsSocket {
|
pub struct FriendsSocket {
|
||||||
@@ -180,27 +179,24 @@ impl FriendsSocket {
|
|||||||
ServerToClientMessage::FriendSocketStoppedListening { .. } => {}, // TODO
|
ServerToClientMessage::FriendSocketStoppedListening { .. } => {}, // TODO
|
||||||
|
|
||||||
ServerToClientMessage::SocketConnected { to_socket, new_socket } => {
|
ServerToClientMessage::SocketConnected { to_socket, new_socket } => {
|
||||||
if let Some(connected_to) = sockets.get(&to_socket) {
|
if let Some(connected_to) = sockets.get(&to_socket)
|
||||||
if let InternalTunnelSocket::Listening(local_addr) = *connected_to.value().clone() {
|
&& let InternalTunnelSocket::Listening(local_addr) = *connected_to.value().clone()
|
||||||
if let Ok(new_stream) = TcpStream::connect(local_addr).await {
|
&& let Ok(new_stream) = TcpStream::connect(local_addr).await {
|
||||||
let (read, write) = new_stream.into_split();
|
let (read, write) = new_stream.into_split();
|
||||||
sockets.insert(new_socket, Arc::new(InternalTunnelSocket::Connected(Mutex::new(write))));
|
sockets.insert(new_socket, Arc::new(InternalTunnelSocket::Connected(Mutex::new(write))));
|
||||||
Self::socket_read_loop(write_handle.clone(), read, new_socket);
|
Self::socket_read_loop(write_handle.clone(), read, new_socket);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
let _ = Self::send_message(&write_handle, ClientToServerMessage::SocketClose { socket: new_socket }).await;
|
let _ = Self::send_message(&write_handle, ClientToServerMessage::SocketClose { socket: new_socket }).await;
|
||||||
},
|
},
|
||||||
ServerToClientMessage::SocketClosed { socket } => {
|
ServerToClientMessage::SocketClosed { socket } => {
|
||||||
sockets.remove_if(&socket, |_, x| matches!(*x.clone(), InternalTunnelSocket::Connected(_)));
|
sockets.remove_if(&socket, |_, x| matches!(*x.clone(), InternalTunnelSocket::Connected(_)));
|
||||||
},
|
},
|
||||||
ServerToClientMessage::SocketData { socket, data } => {
|
ServerToClientMessage::SocketData { socket, data } => {
|
||||||
if let Some(mut socket) = sockets.get_mut(&socket) {
|
if let Some(mut socket) = sockets.get_mut(&socket)
|
||||||
if let InternalTunnelSocket::Connected(ref stream) = *socket.value_mut().clone() {
|
&& let InternalTunnelSocket::Connected(ref stream) = *socket.value_mut().clone() {
|
||||||
let _ = stream.lock().await.write_all(&data).await;
|
let _ = stream.lock().await.write_all(&data).await;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
|
|||||||
let profile_path_str = profile_path_str.clone();
|
let profile_path_str = profile_path_str.clone();
|
||||||
let world = world.clone();
|
let world = world.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Ok(state) = State::get().await {
|
if let Ok(state) = State::get().await
|
||||||
if let Err(e) = attached_world_data::AttachedWorldData::remove_for_world(
|
&& let Err(e) = attached_world_data::AttachedWorldData::remove_for_world(
|
||||||
&profile_path_str,
|
&profile_path_str,
|
||||||
WorldType::Singleplayer,
|
WorldType::Singleplayer,
|
||||||
&world,
|
&world,
|
||||||
@@ -109,7 +109,6 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
|
|||||||
).await {
|
).await {
|
||||||
tracing::warn!("Failed to remove AttachedWorldData for '{world}': {e}")
|
tracing::warn!("Failed to remove AttachedWorldData for '{world}': {e}")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Some(ProfilePayloadType::WorldUpdated { world })
|
Some(ProfilePayloadType::WorldUpdated { world })
|
||||||
@@ -150,8 +149,9 @@ pub(crate) async fn watch_profiles_init(
|
|||||||
) {
|
) {
|
||||||
if let Ok(profiles_dir) = std::fs::read_dir(dirs.profiles_dir()) {
|
if let Ok(profiles_dir) = std::fs::read_dir(dirs.profiles_dir()) {
|
||||||
for profile_dir in profiles_dir {
|
for profile_dir in profiles_dir {
|
||||||
if let Ok(file_name) = profile_dir.map(|x| x.file_name()) {
|
if let Ok(file_name) = profile_dir.map(|x| x.file_name())
|
||||||
if let Some(file_name) = file_name.to_str() {
|
&& let Some(file_name) = file_name.to_str()
|
||||||
|
{
|
||||||
if file_name.starts_with(".DS_Store") {
|
if file_name.starts_with(".DS_Store") {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -161,7 +161,6 @@ pub(crate) async fn watch_profiles_init(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn watch_profile(
|
pub(crate) async fn watch_profile(
|
||||||
profile_path: &str,
|
profile_path: &str,
|
||||||
|
|||||||
@@ -76,11 +76,10 @@ where
|
|||||||
.loaded_config_dir
|
.loaded_config_dir
|
||||||
.clone()
|
.clone()
|
||||||
.and_then(|x| x.to_str().map(|x| x.to_string()))
|
.and_then(|x| x.to_str().map(|x| x.to_string()))
|
||||||
|
&& path != old_launcher_root_str
|
||||||
{
|
{
|
||||||
if path != old_launcher_root_str {
|
|
||||||
settings.custom_dir = Some(path);
|
settings.custom_dir = Some(path);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
settings.prev_custom_dir = Some(old_launcher_root_str.clone());
|
settings.prev_custom_dir = Some(old_launcher_root_str.clone());
|
||||||
|
|
||||||
@@ -136,19 +135,17 @@ where
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(device_token) = minecraft_auth.token {
|
if let Some(device_token) = minecraft_auth.token
|
||||||
if let Ok(private_key) =
|
&& let Ok(private_key) =
|
||||||
SigningKey::from_pkcs8_pem(&device_token.private_key)
|
SigningKey::from_pkcs8_pem(&device_token.private_key)
|
||||||
|
&& let Ok(uuid) = Uuid::parse_str(&device_token.id)
|
||||||
{
|
{
|
||||||
if let Ok(uuid) = Uuid::parse_str(&device_token.id) {
|
|
||||||
DeviceTokenPair {
|
DeviceTokenPair {
|
||||||
token: DeviceToken {
|
token: DeviceToken {
|
||||||
issue_instant: device_token.token.issue_instant,
|
issue_instant: device_token.token.issue_instant,
|
||||||
not_after: device_token.token.not_after,
|
not_after: device_token.token.not_after,
|
||||||
token: device_token.token.token,
|
token: device_token.token.token,
|
||||||
display_claims: device_token
|
display_claims: device_token.token.display_claims,
|
||||||
.token
|
|
||||||
.display_claims,
|
|
||||||
},
|
},
|
||||||
key: DeviceTokenKey {
|
key: DeviceTokenKey {
|
||||||
id: uuid,
|
id: uuid,
|
||||||
@@ -161,8 +158,6 @@ where
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut cached_entries = vec![];
|
let mut cached_entries = vec![];
|
||||||
|
|
||||||
@@ -207,13 +202,12 @@ where
|
|||||||
update_version,
|
update_version,
|
||||||
..
|
..
|
||||||
} = project.metadata
|
} = project.metadata
|
||||||
{
|
&& let Some(file) = version
|
||||||
if let Some(file) = version
|
|
||||||
.files
|
.files
|
||||||
.iter()
|
.iter()
|
||||||
.find(|x| x.hashes.get("sha512") == Some(&sha512))
|
.find(|x| x.hashes.get("sha512") == Some(&sha512))
|
||||||
|
&& let Some(sha1) = file.hashes.get("sha1")
|
||||||
{
|
{
|
||||||
if let Some(sha1) = file.hashes.get("sha1") {
|
|
||||||
if let Ok(metadata) = full_path.metadata() {
|
if let Ok(metadata) = full_path.metadata() {
|
||||||
let file_name = format!(
|
let file_name = format!(
|
||||||
"{}/{}",
|
"{}/{}",
|
||||||
@@ -227,24 +221,24 @@ where
|
|||||||
path: file_name,
|
path: file_name,
|
||||||
size: metadata.len(),
|
size: metadata.len(),
|
||||||
hash: sha1.clone(),
|
hash: sha1.clone(),
|
||||||
project_type: ProjectType::get_from_parent_folder(&full_path),
|
project_type:
|
||||||
|
ProjectType::get_from_parent_folder(
|
||||||
|
&full_path,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
cached_entries.push(CacheValue::File(
|
cached_entries.push(CacheValue::File(CachedFile {
|
||||||
CachedFile {
|
|
||||||
hash: sha1.clone(),
|
hash: sha1.clone(),
|
||||||
project_id: version.project_id.clone(),
|
project_id: version.project_id.clone(),
|
||||||
version_id: version.id.clone(),
|
version_id: version.id.clone(),
|
||||||
},
|
}));
|
||||||
));
|
|
||||||
|
|
||||||
if let Some(update_version) = update_version {
|
if let Some(update_version) = update_version {
|
||||||
let mod_loader: ModLoader =
|
let mod_loader: ModLoader =
|
||||||
profile.metadata.loader.into();
|
profile.metadata.loader.into();
|
||||||
cached_entries.push(
|
cached_entries.push(CacheValue::FileUpdate(
|
||||||
CacheValue::FileUpdate(
|
|
||||||
CachedFileUpdate {
|
CachedFileUpdate {
|
||||||
hash: sha1.clone(),
|
hash: sha1.clone(),
|
||||||
game_version: profile
|
game_version: profile
|
||||||
@@ -252,15 +246,13 @@ where
|
|||||||
.game_version
|
.game_version
|
||||||
.clone(),
|
.clone(),
|
||||||
loaders: vec![
|
loaders: vec![
|
||||||
mod_loader
|
mod_loader.as_str().to_string(),
|
||||||
.as_str()
|
|
||||||
.to_string(),
|
|
||||||
],
|
],
|
||||||
update_version_id:
|
update_version_id: update_version
|
||||||
update_version.id.clone(),
|
.id
|
||||||
|
.clone(),
|
||||||
},
|
},
|
||||||
),
|
));
|
||||||
);
|
|
||||||
|
|
||||||
cached_entries.push(CacheValue::Version(
|
cached_entries.push(CacheValue::Version(
|
||||||
(*update_version).into(),
|
(*update_version).into(),
|
||||||
@@ -280,9 +272,8 @@ where
|
|||||||
badges: 0,
|
badges: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
cached_entries.push(CacheValue::User(
|
cached_entries
|
||||||
user.clone(),
|
.push(CacheValue::User(user.clone()));
|
||||||
));
|
|
||||||
|
|
||||||
TeamMember {
|
TeamMember {
|
||||||
team_id: x.team_id,
|
team_id: x.team_id,
|
||||||
@@ -296,11 +287,8 @@ where
|
|||||||
|
|
||||||
cached_entries.push(CacheValue::Team(members));
|
cached_entries.push(CacheValue::Team(members));
|
||||||
|
|
||||||
cached_entries.push(CacheValue::Version(
|
cached_entries
|
||||||
(*version).into(),
|
.push(CacheValue::Version((*version).into()));
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,17 +320,16 @@ where
|
|||||||
.map(|x| x.id),
|
.map(|x| x.id),
|
||||||
groups: profile.metadata.groups,
|
groups: profile.metadata.groups,
|
||||||
linked_data: profile.metadata.linked_data.and_then(|x| {
|
linked_data: profile.metadata.linked_data.and_then(|x| {
|
||||||
if let Some(project_id) = x.project_id {
|
if let Some(project_id) = x.project_id
|
||||||
if let Some(version_id) = x.version_id {
|
&& let Some(version_id) = x.version_id
|
||||||
if let Some(locked) = x.locked {
|
&& let Some(locked) = x.locked
|
||||||
|
{
|
||||||
return Some(LinkedData {
|
return Some(LinkedData {
|
||||||
project_id,
|
project_id,
|
||||||
version_id,
|
version_id,
|
||||||
locked,
|
locked,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
None
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -393,11 +393,10 @@ impl Credentials {
|
|||||||
..
|
..
|
||||||
},
|
},
|
||||||
) = *err.raw
|
) = *err.raw
|
||||||
|
&& (source.is_connect() || source.is_timeout())
|
||||||
{
|
{
|
||||||
if source.is_connect() || source.is_timeout() {
|
|
||||||
return Ok(Some(creds));
|
return Ok(Some(creds));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
@@ -640,10 +639,9 @@ impl DeviceTokenPair {
|
|||||||
.fetch_optional(exec)
|
.fetch_optional(exec)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(x) = res {
|
if let Some(x) = res
|
||||||
if let Ok(uuid) = Uuid::parse_str(&x.uuid) {
|
&& let Ok(uuid) = Uuid::parse_str(&x.uuid)
|
||||||
if let Ok(private_key) =
|
&& let Ok(private_key) = SigningKey::from_pkcs8_pem(&x.private_key)
|
||||||
SigningKey::from_pkcs8_pem(&x.private_key)
|
|
||||||
{
|
{
|
||||||
return Ok(Some(Self {
|
return Ok(Some(Self {
|
||||||
token: DeviceToken {
|
token: DeviceToken {
|
||||||
@@ -656,9 +654,7 @@ impl DeviceTokenPair {
|
|||||||
.single()
|
.single()
|
||||||
.unwrap_or_else(Utc::now),
|
.unwrap_or_else(Utc::now),
|
||||||
token: x.token,
|
token: x.token,
|
||||||
display_claims: serde_json::from_value(
|
display_claims: serde_json::from_value(x.display_claims)
|
||||||
x.display_claims,
|
|
||||||
)
|
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
},
|
},
|
||||||
key: DeviceTokenKey {
|
key: DeviceTokenKey {
|
||||||
@@ -669,8 +665,6 @@ impl DeviceTokenPair {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
@@ -724,7 +718,7 @@ const MICROSOFT_CLIENT_ID: &str = "00000000402b5328";
|
|||||||
const AUTH_REPLY_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
const AUTH_REPLY_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
||||||
const REQUESTED_SCOPE: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
const REQUESTED_SCOPE: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
||||||
|
|
||||||
struct RequestWithDate<T> {
|
pub struct RequestWithDate<T> {
|
||||||
pub date: DateTime<Utc>,
|
pub date: DateTime<Utc>,
|
||||||
pub value: T,
|
pub value: T,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,8 +360,8 @@ impl Process {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write the throwable if present
|
// Write the throwable if present
|
||||||
if !current_content.is_empty() {
|
if !current_content.is_empty()
|
||||||
if let Err(e) =
|
&& let Err(e) =
|
||||||
Process::append_to_log_file(
|
Process::append_to_log_file(
|
||||||
&log_path,
|
&log_path,
|
||||||
¤t_content,
|
¤t_content,
|
||||||
@@ -374,7 +374,6 @@ impl Process {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
b"log4j:Event" => {
|
b"log4j:Event" => {
|
||||||
in_event = false;
|
in_event = false;
|
||||||
// If no throwable was present, write the log entry at the end of the event
|
// If no throwable was present, write the log entry at the end of the event
|
||||||
@@ -429,8 +428,7 @@ impl Process {
|
|||||||
|
|
||||||
if let Some(timestamp) =
|
if let Some(timestamp) =
|
||||||
current_event.timestamp.as_deref()
|
current_event.timestamp.as_deref()
|
||||||
{
|
&& let Err(e) = Self::maybe_handle_server_join_logging(
|
||||||
if let Err(e) = Self::maybe_handle_server_join_logging(
|
|
||||||
profile_path,
|
profile_path,
|
||||||
timestamp,
|
timestamp,
|
||||||
message
|
message
|
||||||
@@ -439,43 +437,36 @@ impl Process {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Event::Text(mut e)) => {
|
Ok(Event::Text(mut e)) => {
|
||||||
if in_message || in_throwable {
|
if in_message || in_throwable {
|
||||||
if let Ok(text) = e.unescape() {
|
if let Ok(text) = e.xml_content() {
|
||||||
current_content.push_str(&text);
|
current_content.push_str(&text);
|
||||||
}
|
}
|
||||||
} else if !in_event
|
} else if !in_event
|
||||||
&& !e.inplace_trim_end()
|
&& !e.inplace_trim_end()
|
||||||
&& !e.inplace_trim_start()
|
&& !e.inplace_trim_start()
|
||||||
{
|
&& let Ok(text) = e.xml_content()
|
||||||
if let Ok(text) = e.unescape() {
|
&& let Err(e) = Process::append_to_log_file(
|
||||||
if let Err(e) = Process::append_to_log_file(
|
|
||||||
&log_path,
|
&log_path,
|
||||||
&format!("{text}\n"),
|
&format!("{text}\n"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
"Failed to write to log file: {}",
|
"Failed to write to log file: {}",
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Event::CData(e)) => {
|
Ok(Event::CData(e)) => {
|
||||||
if in_message || in_throwable {
|
if (in_message || in_throwable)
|
||||||
if let Ok(text) = e
|
&& let Ok(text) = e.xml_content()
|
||||||
.escape()
|
|
||||||
.map_err(|x| x.into())
|
|
||||||
.and_then(|x| x.unescape())
|
|
||||||
{
|
{
|
||||||
current_content.push_str(&text);
|
current_content.push_str(&text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -720,16 +711,13 @@ impl Process {
|
|||||||
let logs_folder = state.directories.profile_logs_dir(&profile_path);
|
let logs_folder = state.directories.profile_logs_dir(&profile_path);
|
||||||
let log_path = logs_folder.join(LAUNCHER_LOG_PATH);
|
let log_path = logs_folder.join(LAUNCHER_LOG_PATH);
|
||||||
|
|
||||||
if log_path.exists() {
|
if log_path.exists()
|
||||||
if let Err(e) = Process::append_to_log_file(
|
&& let Err(e) = Process::append_to_log_file(
|
||||||
&log_path,
|
&log_path,
|
||||||
&format!("\n# Process exited with status: {mc_exit_status}\n"),
|
&format!("\n# Process exited with status: {mc_exit_status}\n"),
|
||||||
) {
|
)
|
||||||
tracing::warn!(
|
{
|
||||||
"Failed to write exit status to log file: {}",
|
tracing::warn!("Failed to write exit status to log file: {}", e);
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = state.discord_rpc.clear_to_default(true).await;
|
let _ = state.discord_rpc.clear_to_default(true).await;
|
||||||
|
|||||||
@@ -595,8 +595,8 @@ impl Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self, semaphore, icon))]
|
#[tracing::instrument(skip(self, semaphore, icon))]
|
||||||
pub async fn set_icon<'a>(
|
pub async fn set_icon(
|
||||||
&'a mut self,
|
&mut self,
|
||||||
cache_dir: &Path,
|
cache_dir: &Path,
|
||||||
semaphore: &IoSemaphore,
|
semaphore: &IoSemaphore,
|
||||||
icon: bytes::Bytes,
|
icon: bytes::Bytes,
|
||||||
@@ -629,8 +629,8 @@ impl Profile {
|
|||||||
{
|
{
|
||||||
let subdirectory =
|
let subdirectory =
|
||||||
subdirectory.map_err(io::IOError::from)?.path();
|
subdirectory.map_err(io::IOError::from)?.path();
|
||||||
if subdirectory.is_file() {
|
if subdirectory.is_file()
|
||||||
if let Some(file_name) = subdirectory
|
&& let Some(file_name) = subdirectory
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|x| x.to_str())
|
.and_then(|x| x.to_str())
|
||||||
{
|
{
|
||||||
@@ -647,7 +647,6 @@ impl Profile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if profile.install_stage == ProfileInstallStage::MinecraftInstalling
|
if profile.install_stage == ProfileInstallStage::MinecraftInstalling
|
||||||
{
|
{
|
||||||
@@ -901,8 +900,8 @@ impl Profile {
|
|||||||
{
|
{
|
||||||
let subdirectory =
|
let subdirectory =
|
||||||
subdirectory.map_err(io::IOError::from)?.path();
|
subdirectory.map_err(io::IOError::from)?.path();
|
||||||
if subdirectory.is_file() {
|
if subdirectory.is_file()
|
||||||
if let Some(file_name) =
|
&& let Some(file_name) =
|
||||||
subdirectory.file_name().and_then(|x| x.to_str())
|
subdirectory.file_name().and_then(|x| x.to_str())
|
||||||
{
|
{
|
||||||
let file_size = subdirectory
|
let file_size = subdirectory
|
||||||
@@ -928,7 +927,6 @@ impl Profile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let file_hashes = CachedEntry::get_file_hash_many(
|
let file_hashes = CachedEntry::get_file_hash_many(
|
||||||
&keys.iter().map(|s| &*s.cache_key).collect::<Vec<_>>(),
|
&keys.iter().map(|s| &*s.cache_key).collect::<Vec<_>>(),
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(bytes, semaphore))]
|
#[tracing::instrument(skip(bytes, semaphore))]
|
||||||
pub async fn write<'a>(
|
pub async fn write(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
semaphore: &IoSemaphore,
|
semaphore: &IoSemaphore,
|
||||||
|
|||||||
@@ -191,13 +191,13 @@ async fn get_all_autoinstalled_jre_path() -> Result<HashSet<PathBuf>, JREError>
|
|||||||
let mut jre_paths = HashSet::new();
|
let mut jre_paths = HashSet::new();
|
||||||
let base_path = state.directories.java_versions_dir();
|
let base_path = state.directories.java_versions_dir();
|
||||||
|
|
||||||
if base_path.is_dir() {
|
if base_path.is_dir()
|
||||||
if let Ok(dir) = std::fs::read_dir(base_path) {
|
&& let Ok(dir) = std::fs::read_dir(base_path)
|
||||||
|
{
|
||||||
for entry in dir.flatten() {
|
for entry in dir.flatten() {
|
||||||
let file_path = entry.path().join("bin");
|
let file_path = entry.path().join("bin");
|
||||||
|
|
||||||
if let Ok(contents) =
|
if let Ok(contents) = std::fs::read_to_string(file_path.clone())
|
||||||
std::fs::read_to_string(file_path.clone())
|
|
||||||
{
|
{
|
||||||
let entry = entry.path().join(contents);
|
let entry = entry.path().join(contents);
|
||||||
jre_paths.insert(entry);
|
jre_paths.insert(entry);
|
||||||
@@ -210,7 +210,6 @@ async fn get_all_autoinstalled_jre_path() -> Result<HashSet<PathBuf>, JREError>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(jre_paths)
|
Ok(jre_paths)
|
||||||
})
|
})
|
||||||
@@ -300,8 +299,9 @@ pub async fn check_java_at_filepath(path: &Path) -> crate::Result<JavaVersion> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract version info from it
|
// Extract version info from it
|
||||||
if let Some(arch) = java_arch {
|
if let Some(arch) = java_arch
|
||||||
if let Some(version) = java_version {
|
&& let Some(version) = java_version
|
||||||
|
{
|
||||||
if let Ok(version) = extract_java_version(version) {
|
if let Ok(version) = extract_java_version(version) {
|
||||||
let path = java.to_string_lossy().to_string();
|
let path = java.to_string_lossy().to_string();
|
||||||
return Ok(JavaVersion {
|
return Ok(JavaVersion {
|
||||||
@@ -314,7 +314,6 @@ pub async fn check_java_at_filepath(path: &Path) -> crate::Result<JavaVersion> {
|
|||||||
|
|
||||||
return Err(JREError::InvalidJREVersion(version.to_owned()).into());
|
return Err(JREError::InvalidJREVersion(version.to_owned()).into());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Err(JREError::FailedJavaCheck(java).into())
|
Err(JREError::FailedJavaCheck(java).into())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,12 @@ pub fn is_feature_supported_in(
|
|||||||
if part_version == part_first_release {
|
if part_version == part_first_release {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Ok(part_version) = part_version.parse::<u32>() {
|
if let Ok(part_version) = part_version.parse::<u32>()
|
||||||
if let Ok(part_first_release) = part_first_release.parse::<u32>() {
|
&& let Ok(part_first_release) = part_first_release.parse::<u32>()
|
||||||
if part_version > part_first_release {
|
&& part_version > part_first_release
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
+18
-111
@@ -1,8 +1,6 @@
|
|||||||
<!-- TODO: After checklist v1.5, move everything into src directory. -->
|
|
||||||
|
|
||||||
# @modrinth/moderation
|
# @modrinth/moderation
|
||||||
|
|
||||||
This package contains both the moderation checklist system used by moderators for reviewing projects on Modrinth, and the publishing checklist (nag system) that provides automated feedback to project authors during the submission process.
|
This package contains the moderation checklist system used for reviewing projects on Modrinth. It provides a structured and transparent way to define moderation stages, actions, and messages that are displayed to moderators during the review process.
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
@@ -11,30 +9,22 @@ The package is organized as follows:
|
|||||||
```
|
```
|
||||||
/packages/moderation/
|
/packages/moderation/
|
||||||
├── data/
|
├── data/
|
||||||
│ ├── checklist.ts # Main moderation checklist definition - imports and exports all stages
|
│ ├── checklist.ts # Main checklist definition - imports and exports all stages
|
||||||
│ ├── messages/ # Markdown files containing message templates for moderation
|
│ ├── messages/ # Markdown files containing message templates
|
||||||
│ │ ├── title/ # Messages for the title stage
|
│ │ ├── title/ # Messages for the title stage
|
||||||
│ │ ├── description/ # Messages for the description stage
|
│ │ ├── description/ # Messages for the description stage
|
||||||
│ │ └── ... # One directory per stage
|
│ │ └── ... # One directory per stage
|
||||||
│ ├── stages/ # Moderation stage definition files
|
│ └── stages/ # Stage definition files
|
||||||
│ │ ├── title.ts # Title stage definition
|
│ ├── title.ts # Title stage definition
|
||||||
│ │ ├── description.ts # Description stage definition
|
│ ├── description.ts # Description stage definition
|
||||||
│ │ └── ... # One file per stage
|
│ └── ... # One file per stage
|
||||||
│ └── nags/ # Publishing checklist (nag system) files
|
|
||||||
│ ├── core.ts # Core nags (required fields, basic validation)
|
|
||||||
│ └── ...
|
|
||||||
└── types/ # Type definitions
|
└── types/ # Type definitions
|
||||||
├── actions.ts # Action-related types (moderation)
|
├── actions.ts # Action-related types
|
||||||
├── messages.ts # Message-related types (moderation)
|
├── messages.ts # Message-related types
|
||||||
├── stage.ts # Stage-related types (moderation)
|
└── stage.ts # Stage-related types
|
||||||
└── nags.ts # Nag-related types (publishing checklist)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Moderation Checklist System
|
## Stages
|
||||||
|
|
||||||
The moderation checklist provides a structured and transparent way to define moderation stages, actions, and messages that are displayed to moderators during the review process.
|
|
||||||
|
|
||||||
### Stages
|
|
||||||
|
|
||||||
A stage represents a discrete step in the moderation process, like checking a project's title, description, or links. Each stage has:
|
A stage represents a discrete step in the moderation process, like checking a project's title, description, or links. Each stage has:
|
||||||
|
|
||||||
@@ -45,7 +35,7 @@ A stage represents a discrete step in the moderation process, like checking a pr
|
|||||||
|
|
||||||
Stages are defined in individual files in the `data/stages` directory and are assembled into the complete checklist in `data/checklist.ts`.
|
Stages are defined in individual files in the `data/stages` directory and are assembled into the complete checklist in `data/checklist.ts`.
|
||||||
|
|
||||||
### Actions
|
## Actions
|
||||||
|
|
||||||
Actions represent decisions moderators can make for each stage. They can be buttons, dropdowns, toggles, etc. Actions can have:
|
Actions represent decisions moderators can make for each stage. They can be buttons, dropdowns, toggles, etc. Actions can have:
|
||||||
|
|
||||||
@@ -57,11 +47,11 @@ Actions represent decisions moderators can make for each stage. They can be butt
|
|||||||
|
|
||||||
Each action requires a unique `id` field that is used for conditional logic and action relationships. The `suggestedStatus` and `severity` fields help determine the overall moderation outcome.
|
Each action requires a unique `id` field that is used for conditional logic and action relationships. The `suggestedStatus` and `severity` fields help determine the overall moderation outcome.
|
||||||
|
|
||||||
### Messages
|
## Messages
|
||||||
|
|
||||||
Messages are the actual text that will be included in communications to project authors. To promote maintainability and reuse, messages are stored as Markdown files in the `data/messages` directory, organized by stage.
|
Messages are the actual text that will be included in communications to project authors. To promote maintainability and reuse, messages are stored as Markdown files in the `data/messages` directory, organized by stage.
|
||||||
|
|
||||||
#### Variable replacement
|
### Variable replacement
|
||||||
|
|
||||||
You can use variables in your messages that will be replaced with user input:
|
You can use variables in your messages that will be replaced with user input:
|
||||||
|
|
||||||
@@ -91,11 +81,11 @@ More text after the variable.
|
|||||||
|
|
||||||
The `%MESSAGE%` placeholder will be replaced with the text entered by the moderator.
|
The `%MESSAGE%` placeholder will be replaced with the text entered by the moderator.
|
||||||
|
|
||||||
### Conditional logic
|
## Conditional logic
|
||||||
|
|
||||||
The moderation system supports conditional behavior that changes based on the selection of other actions.
|
The moderation system supports conditional behavior that changes based on the selection of other actions.
|
||||||
|
|
||||||
#### Conditional messages
|
### Conditional messages
|
||||||
|
|
||||||
You can define different messages for an action based on other selected actions:
|
You can define different messages for an action based on other selected actions:
|
||||||
|
|
||||||
@@ -118,7 +108,7 @@ You can define different messages for an action based on other selected actions:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Enabling and disabling actions
|
### Enabling and disabling actions
|
||||||
|
|
||||||
Actions can enable or disable other actions when selected:
|
Actions can enable or disable other actions when selected:
|
||||||
|
|
||||||
@@ -141,7 +131,7 @@ Actions can enable or disable other actions when selected:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Conditional text inputs
|
### Conditional text inputs
|
||||||
|
|
||||||
Text inputs can be conditionally shown based on selected actions:
|
Text inputs can be conditionally shown based on selected actions:
|
||||||
|
|
||||||
@@ -157,86 +147,3 @@ relevantExtraInput: [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Publishing Checklist (Nag System)
|
|
||||||
|
|
||||||
The nag system provides automated feedback to project authors during the submission process, helping them improve their projects before they reach moderation. It analyzes project data and provides suggestions, warnings, and requirements.
|
|
||||||
|
|
||||||
### Nags
|
|
||||||
|
|
||||||
A nag represents a specific issue or suggestion for improvement. Each nag has:
|
|
||||||
|
|
||||||
- A unique `id` for identification
|
|
||||||
- A `title` and `description` displayed to the user
|
|
||||||
- A `status` indicating severity: `'required'`, `'warning'`, or `'suggestion'`
|
|
||||||
- A `shouldShow` function that determines when the nag should be displayed
|
|
||||||
- An optional `link` to help users address the issue
|
|
||||||
|
|
||||||
### Internationalization
|
|
||||||
|
|
||||||
Use vintl's `defineMessage` syntax.
|
|
||||||
|
|
||||||
If you want to use context in the messages, you can do so like this:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
|
|
||||||
return formatMessage(defineMessage(...), {
|
|
||||||
length: context.project.body?.length || 0,
|
|
||||||
minChars: MIN_DESCRIPTION_CHARS,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Nag Context
|
|
||||||
|
|
||||||
The `NagContext` type provides access to:
|
|
||||||
|
|
||||||
- `project`: Current project data
|
|
||||||
- `versions`: Project versions
|
|
||||||
- `tags`: Frontend "tags" (generated state)
|
|
||||||
- `currentRoute`: Current page route
|
|
||||||
- and other data...
|
|
||||||
|
|
||||||
### Adding New Nags
|
|
||||||
|
|
||||||
To add a new nag:
|
|
||||||
|
|
||||||
1. Add the nag definition to the appropriate category file (or make a new category file and add it to `data/nags.ts`)
|
|
||||||
2. Add corresponding i18n messages to the `.i18n.ts` file
|
|
||||||
3. Implement the `shouldShow` logic based on project state
|
|
||||||
4. Add appropriate links to help users resolve the issue
|
|
||||||
5. Run `pnpm run fix` to fix lint issues & generate the root locale index.json file.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// In description.ts
|
|
||||||
{
|
|
||||||
id: 'new-nag',
|
|
||||||
title: messages.newNagTitle,
|
|
||||||
description: messages.newNagDescription,
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
// Your validation logic here
|
|
||||||
return someCondition
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/description',
|
|
||||||
title: messages.editDescriptionTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// In description.i18n.ts
|
|
||||||
newNagTitle: {
|
|
||||||
id: 'nags.new-nag.title',
|
|
||||||
defaultMessage: 'New Nag Title',
|
|
||||||
},
|
|
||||||
newNagDescription: {
|
|
||||||
id: 'nags.new-nag.description',
|
|
||||||
defaultMessage: 'Description of the new nag issue.',
|
|
||||||
```
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { Nag } from '../types/nags'
|
|
||||||
import { coreNags } from './nags/core'
|
|
||||||
import { descriptionNags } from './nags/description'
|
|
||||||
import { linksNags } from './nags/links'
|
|
||||||
import { tagsNags } from './nags/tags'
|
|
||||||
|
|
||||||
export default [...coreNags, ...linksNags, ...descriptionNags, ...tagsNags] as Nag[]
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
import type { Nag, NagContext } from '../../types/nags'
|
|
||||||
import { formatProjectType } from '@modrinth/utils'
|
|
||||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
|
||||||
|
|
||||||
export const coreNags: Nag[] = [
|
|
||||||
{
|
|
||||||
id: 'moderator-feedback',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.moderator-feedback.title',
|
|
||||||
defaultMessage: 'Review moderator feedback',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.moderator-feedback.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Review and address all concerns from the moderation team before resubmitting.',
|
|
||||||
}),
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) =>
|
|
||||||
context.tags.rejectedStatuses.includes(context.project.status),
|
|
||||||
link: {
|
|
||||||
path: 'moderation',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.moderation.title',
|
|
||||||
defaultMessage: 'Visit moderation thread',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-moderation',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'upload-version',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.upload-version.title',
|
|
||||||
defaultMessage: 'Upload a version',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.upload-version.description',
|
|
||||||
defaultMessage: 'At least one version is required for a project to be submitted for review.',
|
|
||||||
}),
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => context.versions.length < 1,
|
|
||||||
link: {
|
|
||||||
path: 'versions',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.versions.title',
|
|
||||||
defaultMessage: 'Visit versions page',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-versions',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'add-description',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.add-description.title',
|
|
||||||
defaultMessage: 'Add a description',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.add-description.description',
|
|
||||||
defaultMessage:
|
|
||||||
"A description that clearly describes the project's purpose and function is required.",
|
|
||||||
}),
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => context.project.body === '',
|
|
||||||
link: {
|
|
||||||
path: 'settings/description',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.settings.description.title',
|
|
||||||
defaultMessage: 'Visit description settings',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'add-icon',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.add-icon.title',
|
|
||||||
defaultMessage: 'Add an icon',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.add-icon.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Adding a unique, relevant, and engaging icon makes your project identifiable and helps it stand out.',
|
|
||||||
}),
|
|
||||||
status: 'suggestion',
|
|
||||||
shouldShow: (context: NagContext) => !context.project.icon_url,
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.settings.title',
|
|
||||||
defaultMessage: 'Visit general settings',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'feature-gallery-image',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.feature-gallery-image.title',
|
|
||||||
defaultMessage: 'Feature a gallery image',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.feature-gallery-image.description',
|
|
||||||
defaultMessage:
|
|
||||||
'The featured gallery image is often how your project makes its first impression.',
|
|
||||||
}),
|
|
||||||
status: 'suggestion',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const featuredGalleryImage = context.project.gallery?.find((img) => img.featured)
|
|
||||||
return context.project?.gallery?.length === 0 || !featuredGalleryImage
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'gallery',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.gallery.title',
|
|
||||||
defaultMessage: 'Visit gallery page',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-gallery',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'select-tags',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.select-tags.title',
|
|
||||||
defaultMessage: 'Select tags',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.select-tags.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Select the tags that correctly apply to your project to help the right users find it.',
|
|
||||||
}),
|
|
||||||
status: 'suggestion',
|
|
||||||
shouldShow: (context: NagContext) =>
|
|
||||||
context.project.versions.length > 0 && context.project.categories.length < 1,
|
|
||||||
link: {
|
|
||||||
path: 'settings/tags',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.settings.tags.title',
|
|
||||||
defaultMessage: 'Visit tag settings',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'add-links',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.add-links.title',
|
|
||||||
defaultMessage: 'Add external links',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.add-links.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Add any relevant links targeted outside of Modrinth, such as source code, an issue tracker, or a Discord invite.',
|
|
||||||
}),
|
|
||||||
status: 'suggestion',
|
|
||||||
shouldShow: (context: NagContext) =>
|
|
||||||
!(
|
|
||||||
context.project.issues_url ||
|
|
||||||
context.project.source_url ||
|
|
||||||
context.project.wiki_url ||
|
|
||||||
context.project.discord_url ||
|
|
||||||
context.project.donation_urls.length > 0
|
|
||||||
),
|
|
||||||
link: {
|
|
||||||
path: 'settings/links',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.settings.links.title',
|
|
||||||
defaultMessage: 'Visit links settings',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'select-environments',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.select-environments.title',
|
|
||||||
defaultMessage: 'Select supported environments',
|
|
||||||
}),
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
|
|
||||||
return formatMessage(
|
|
||||||
defineMessage({
|
|
||||||
id: 'nags.select-environments.description',
|
|
||||||
defaultMessage: `Select if the {projectType} functions on the client-side and/or server-side.`,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const excludedTypes = ['resourcepack', 'plugin', 'shader', 'datapack']
|
|
||||||
return (
|
|
||||||
context.project.versions.length > 0 &&
|
|
||||||
!excludedTypes.includes(context.project.project_type) &&
|
|
||||||
(context.project.client_side === 'unknown' ||
|
|
||||||
context.project.server_side === 'unknown' ||
|
|
||||||
(context.project.client_side === 'unsupported' &&
|
|
||||||
context.project.server_side === 'unsupported'))
|
|
||||||
)
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.settings.environments.title',
|
|
||||||
defaultMessage: 'Visit general settings',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'select-license',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.select-license.title',
|
|
||||||
defaultMessage: 'Select a license',
|
|
||||||
}),
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
|
|
||||||
return formatMessage(
|
|
||||||
defineMessage({
|
|
||||||
id: 'nags.select-license.description',
|
|
||||||
defaultMessage: 'Select the license your {projectType} is distributed under.',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => context.project.license.id === 'LicenseRef-Unknown',
|
|
||||||
link: {
|
|
||||||
path: 'settings/license',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.settings.license.title',
|
|
||||||
defaultMessage: 'Visit license settings',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-license',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -1,343 +0,0 @@
|
|||||||
import { renderHighlightedString } from '@modrinth/utils'
|
|
||||||
import type { Nag, NagContext } from '../../types/nags'
|
|
||||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
|
||||||
|
|
||||||
export const MIN_DESCRIPTION_CHARS = 200
|
|
||||||
export const MAX_HEADER_LENGTH = 80
|
|
||||||
export const MIN_SUMMARY_CHARS = 30
|
|
||||||
export const MIN_CHARS_PER_IMAGE = 60
|
|
||||||
|
|
||||||
export function analyzeHeaderLength(markdown: string): {
|
|
||||||
hasLongHeaders: boolean
|
|
||||||
longHeaders: string[]
|
|
||||||
} {
|
|
||||||
if (!markdown) return { hasLongHeaders: false, longHeaders: [] }
|
|
||||||
|
|
||||||
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
|
||||||
|
|
||||||
const headerRegex = /^(#{1,3})\s+(.+)$/gm
|
|
||||||
const headers = [...withoutCodeBlocks.matchAll(headerRegex)]
|
|
||||||
|
|
||||||
const longHeaders: string[] = []
|
|
||||||
|
|
||||||
headers.forEach((match) => {
|
|
||||||
const headerText = match[2].trim()
|
|
||||||
const sentenceEnders = /[.!?]+/g
|
|
||||||
const sentences = headerText.split(sentenceEnders).filter((s) => s.trim().length > 0)
|
|
||||||
|
|
||||||
const isVeryLong = headerText.length > MAX_HEADER_LENGTH
|
|
||||||
const hasMultipleSentences = sentences.length > 1
|
|
||||||
|
|
||||||
if (isVeryLong || hasMultipleSentences) {
|
|
||||||
longHeaders.push(headerText)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasLongHeaders: longHeaders.length > 0,
|
|
||||||
longHeaders,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function analyzeImageContent(markdown: string): {
|
|
||||||
imageHeavy: boolean
|
|
||||||
hasEmptyAltText: boolean
|
|
||||||
} {
|
|
||||||
if (!markdown) return { imageHeavy: false, hasEmptyAltText: false }
|
|
||||||
|
|
||||||
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
|
||||||
|
|
||||||
const imageRegex = /!\[([^\]]*)\]\([^)]+\)/g
|
|
||||||
const images = [...withoutCodeBlocks.matchAll(imageRegex)]
|
|
||||||
|
|
||||||
const htmlImageRegex = /<img[^>]*>/gi
|
|
||||||
const htmlImages = [...withoutCodeBlocks.matchAll(htmlImageRegex)]
|
|
||||||
|
|
||||||
const totalImages = images.length + htmlImages.length
|
|
||||||
if (totalImages === 0) return { imageHeavy: false, hasEmptyAltText: false }
|
|
||||||
|
|
||||||
const textWithoutImages = withoutCodeBlocks
|
|
||||||
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '')
|
|
||||||
.replace(/<img[^>]*>/gi, '')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
const textLength = textWithoutImages.length
|
|
||||||
const recommendedTextLength = MIN_CHARS_PER_IMAGE * totalImages
|
|
||||||
|
|
||||||
const imageHeavy =
|
|
||||||
recommendedTextLength > MIN_DESCRIPTION_CHARS && textLength < recommendedTextLength
|
|
||||||
|
|
||||||
const hasEmptyAltText =
|
|
||||||
images.some((match) => !match[1]?.trim()) ||
|
|
||||||
htmlImages.some((match) => {
|
|
||||||
const altMatch = match[0].match(/alt\s*=\s*["']([^"']*)["']/i)
|
|
||||||
return !altMatch || !altMatch[1]?.trim()
|
|
||||||
})
|
|
||||||
|
|
||||||
return { imageHeavy, hasEmptyAltText }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function countText(markdown: string): number {
|
|
||||||
const htmlString = renderHighlightedString(markdown)
|
|
||||||
const parser = new DOMParser()
|
|
||||||
const doc = parser.parseFromString(htmlString, 'text/html')
|
|
||||||
const walker = document.createTreeWalker(doc, NodeFilter.SHOW_TEXT)
|
|
||||||
|
|
||||||
const textList: string[] = []
|
|
||||||
let currentNode: Node | null = walker.currentNode
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
if (currentNode.textContent !== null) {
|
|
||||||
textList.push(currentNode.textContent)
|
|
||||||
}
|
|
||||||
currentNode = walker.nextNode()
|
|
||||||
}
|
|
||||||
|
|
||||||
return textList.join(' ').trim().length
|
|
||||||
}
|
|
||||||
|
|
||||||
export const descriptionNags: Nag[] = [
|
|
||||||
{
|
|
||||||
id: 'description-too-short',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.description-too-short.title',
|
|
||||||
defaultMessage: 'Expand the description',
|
|
||||||
}),
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const readableLength = countText(context.project.body || '')
|
|
||||||
|
|
||||||
return formatMessage(
|
|
||||||
defineMessage({
|
|
||||||
id: 'nags.description-too-short.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Your description is {length} readable characters. At least {minChars} characters is recommended to create a clear and informative description.',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
length: readableLength,
|
|
||||||
minChars: MIN_DESCRIPTION_CHARS,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const readableLength = countText(context.project.body || '')
|
|
||||||
return readableLength < MIN_DESCRIPTION_CHARS && readableLength > 0
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/description',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.edit-description.title',
|
|
||||||
defaultMessage: 'Edit description',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'long-headers',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.long-headers.title',
|
|
||||||
defaultMessage: 'Shorten headers',
|
|
||||||
}),
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const { longHeaders } = analyzeHeaderLength(context.project.body || '')
|
|
||||||
const count = longHeaders.length
|
|
||||||
|
|
||||||
return formatMessage(
|
|
||||||
defineMessage({
|
|
||||||
id: 'nags.long-headers.description',
|
|
||||||
defaultMessage:
|
|
||||||
'{count, plural, one {# header} other {# headers}} in your description {count, plural, one {is} other {are}} too long. Headers should be concise and act as section titles, not full sentences.',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
count,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const { hasLongHeaders } = analyzeHeaderLength(context.project.body || '')
|
|
||||||
return hasLongHeaders
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/description',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.edit-description.title',
|
|
||||||
defaultMessage: 'Edit description',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'summary-too-short',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.summary-too-short.title',
|
|
||||||
defaultMessage: 'Expand the summary',
|
|
||||||
}),
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
|
|
||||||
return formatMessage(
|
|
||||||
defineMessage({
|
|
||||||
id: 'nags.summary-too-short.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Your summary is {length} characters. At least {minChars} characters is recommended to create an informative and enticing summary.',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
length: context.project.description?.length || 0,
|
|
||||||
minChars: MIN_SUMMARY_CHARS,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const summaryLength = context.project.description?.trim()?.length || 0
|
|
||||||
return summaryLength < MIN_SUMMARY_CHARS && summaryLength !== 0
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.edit-summary.title',
|
|
||||||
defaultMessage: 'Edit summary',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'minecraft-title-clause',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.minecraft-title-clause.title',
|
|
||||||
defaultMessage: 'Avoid brand infringement',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.minecraft-title-clause.description',
|
|
||||||
defaultMessage: `Projects must not use Minecraft's branding or include "Minecraft" as a significant part of the name.`,
|
|
||||||
}),
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const title = context.project.title?.toLowerCase() || ''
|
|
||||||
const wordsInTitle = title.split(' ').filter((word) => word.length > 0)
|
|
||||||
return title.includes('minecraft') && title.length > 0 && wordsInTitle.length <= 3
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.edit-title.title',
|
|
||||||
defaultMessage: 'Edit title',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'title-contains-technical-info',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.title-contains-technical-info.title',
|
|
||||||
defaultMessage: 'Clean up the name',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.title-contains-technical-info.description',
|
|
||||||
defaultMessage:
|
|
||||||
"Keeping your project's Name clean and makes it memorable easier to find. Version and loader information is automatically displayed alongside your project.",
|
|
||||||
}),
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const title = context.project.title?.toLowerCase() || ''
|
|
||||||
if (!title) return false
|
|
||||||
|
|
||||||
const loaderNames =
|
|
||||||
context.tags.loaders?.map((loader: { name: string }) => loader.name?.toLowerCase()) || []
|
|
||||||
const hasLoader = loaderNames.some((loader) => loader && title.includes(loader.toLowerCase()))
|
|
||||||
const versionPatterns = [/\b1\.\d+(\.\d+)?\b/]
|
|
||||||
const hasVersionPattern = versionPatterns.some((pattern) => pattern.test(title))
|
|
||||||
|
|
||||||
return hasLoader || hasVersionPattern
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.edit-title.title',
|
|
||||||
defaultMessage: 'Edit title',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'summary-same-as-title',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.summary-same-as-title.title',
|
|
||||||
defaultMessage: 'Make the summary unique',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.summary-same-as-title.description',
|
|
||||||
defaultMessage:
|
|
||||||
"Your summary can not be the same as your project's Name. It's important to create an informative and enticing Summary.",
|
|
||||||
}),
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const title = context.project.title?.trim() || ''
|
|
||||||
const summary = context.project.description?.trim() || ''
|
|
||||||
return title === summary && title.length > 0 && summary.length > 0
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.edit-summary.title',
|
|
||||||
defaultMessage: 'Edit summary',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Don't like this one, is this needed?
|
|
||||||
id: 'image-heavy-description',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.image-heavy-description.title',
|
|
||||||
defaultMessage: 'Ensure accessibility',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.image-heavy-description.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Your Description should contain sufficient plain text or image alt-text, keeping it accessible to those using screen readers or with slow internet connections.',
|
|
||||||
}),
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const { imageHeavy } = analyzeImageContent(context.project.body || '')
|
|
||||||
return imageHeavy
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/description',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.edit-description.title',
|
|
||||||
defaultMessage: 'Edit description',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'missing-alt-text',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.missing-alt-text.title',
|
|
||||||
defaultMessage: 'Add image alt text',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.missing-alt-text.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Some of your images are missing alt text, which is important for accessibility, especially for visually impaired users.',
|
|
||||||
}),
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const { hasEmptyAltText } = analyzeImageContent(context.project.body || '')
|
|
||||||
return hasEmptyAltText
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/description',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.edit-description.title',
|
|
||||||
defaultMessage: 'Edit description',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './core'
|
|
||||||
export * from './links'
|
|
||||||
export * from './description'
|
|
||||||
export * from './tags'
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
import type { Nag, NagContext } from '../../types/nags'
|
|
||||||
import { formatProjectType } from '@modrinth/utils'
|
|
||||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
|
||||||
|
|
||||||
export const commonLinkDomains = {
|
|
||||||
source: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org', 'git.sr.ht'],
|
|
||||||
issues: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org', 'docs.google.com'],
|
|
||||||
discord: ['discord.gg', 'discord.com', 'dsc.gg'],
|
|
||||||
licenseBlocklist: [
|
|
||||||
'youtube.com',
|
|
||||||
'youtu.be',
|
|
||||||
'modrinth.com',
|
|
||||||
'curseforge.com',
|
|
||||||
'twitter.com',
|
|
||||||
'x.com',
|
|
||||||
'discord.gg',
|
|
||||||
'discord.com',
|
|
||||||
'instagram.com',
|
|
||||||
'facebook.com',
|
|
||||||
'tiktok.com',
|
|
||||||
'reddit.com',
|
|
||||||
'twitch.tv',
|
|
||||||
'patreon.com',
|
|
||||||
'ko-fi.com',
|
|
||||||
'paypal.com',
|
|
||||||
'buymeacoffee.com',
|
|
||||||
'google.com',
|
|
||||||
'example.com',
|
|
||||||
't.me',
|
|
||||||
],
|
|
||||||
linkShorteners: ['bit.ly', 'adf.ly', 'tinyurl.com', 'short.io', 'is.gd'],
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCommonUrl(url: string | null, commonDomains: string[]): boolean {
|
|
||||||
if (url === null || url === '') return true
|
|
||||||
try {
|
|
||||||
const domain = new URL(url).hostname.toLowerCase()
|
|
||||||
return commonDomains.some((allowed) => domain.includes(allowed))
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCommonUrlOfType(url: string | null, commonDomains: string[]): boolean {
|
|
||||||
if (url === null || url === '') return false
|
|
||||||
return isCommonUrl(url, commonDomains)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isDiscordUrl(url: string | null): boolean {
|
|
||||||
return isCommonUrlOfType(url, commonLinkDomains.discord)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLinkShortener(url: string | null): boolean {
|
|
||||||
return isCommonUrlOfType(url, commonLinkDomains.linkShorteners)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isUncommonLicenseUrl(url: string | null): boolean {
|
|
||||||
return isCommonUrlOfType(url, commonLinkDomains.licenseBlocklist)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const linksNags: Nag[] = [
|
|
||||||
{
|
|
||||||
id: 'verify-external-links',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.verify-external-links.title',
|
|
||||||
defaultMessage: 'Verify external links',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.verify-external-links.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Some of your external links may be using domains that are inappropriate for that type of link.',
|
|
||||||
}),
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
return (
|
|
||||||
!isCommonUrl(context.project.source_url, commonLinkDomains.source) ||
|
|
||||||
!isCommonUrl(context.project.issues_url, commonLinkDomains.issues) ||
|
|
||||||
!isCommonUrl(context.project.discord_url, commonLinkDomains.discord)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/links',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.visit-links-settings.title',
|
|
||||||
defaultMessage: 'Visit links settings',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'misused-discord-link',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.misused-discord-link.title',
|
|
||||||
defaultMessage: 'Move Discord invite',
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.misused-discord-link-description',
|
|
||||||
defaultMessage:
|
|
||||||
'Discord invites can not be used for other link types. Please put your Discord link in the Discord Invite link field only.',
|
|
||||||
}),
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) =>
|
|
||||||
isDiscordUrl(context.project.source_url) ||
|
|
||||||
isDiscordUrl(context.project.issues_url) ||
|
|
||||||
isDiscordUrl(context.project.wiki_url),
|
|
||||||
link: {
|
|
||||||
path: 'settings/links',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.visit-links-settings.title',
|
|
||||||
defaultMessage: 'Visit links settings',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'link-shortener-usage',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.link-shortener-usage.title',
|
|
||||||
defaultMessage: "Don't use link shorteners",
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
|
||||||
id: 'nags.link-shortener-usage.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Use of link shorteners or other methods to obscure where a link may lead in your external links or license link is prohibited, please only use appropriate full length links.',
|
|
||||||
}),
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) =>
|
|
||||||
isLinkShortener(context.project.source_url) ||
|
|
||||||
isLinkShortener(context.project.issues_url) ||
|
|
||||||
isLinkShortener(context.project.wiki_url) ||
|
|
||||||
Boolean(context.project.license.url && isLinkShortener(context.project.license.url)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'invalid-license-url',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.invalid-license-url.title',
|
|
||||||
defaultMessage: 'Add a valid license link',
|
|
||||||
}),
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const licenseUrl = context.project.license.url
|
|
||||||
|
|
||||||
if (!licenseUrl) {
|
|
||||||
return formatMessage(
|
|
||||||
defineMessage({
|
|
||||||
id: 'nags.invalid-license-url.description.default',
|
|
||||||
defaultMessage: 'License URL is invalid.',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const domain = new URL(licenseUrl).hostname.toLowerCase()
|
|
||||||
return formatMessage(
|
|
||||||
defineMessage({
|
|
||||||
id: 'nags.invalid-license-url.description.domain',
|
|
||||||
defaultMessage:
|
|
||||||
'Your license URL points to {domain}, which is not appropriate for license information. License URLs should link directly to your license file, not social media, gaming platforms, etc.',
|
|
||||||
}),
|
|
||||||
{ domain },
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return formatMessage(
|
|
||||||
defineMessage({
|
|
||||||
id: 'nags.invalid-license-url.description.malformed',
|
|
||||||
defaultMessage:
|
|
||||||
'Your license URL appears to be malformed. Please provide a valid URL to your license text.',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const licenseUrl = context.project.license.url
|
|
||||||
if (!licenseUrl) return false
|
|
||||||
|
|
||||||
const isBlocklisted = isUncommonLicenseUrl(licenseUrl)
|
|
||||||
|
|
||||||
try {
|
|
||||||
new URL(licenseUrl)
|
|
||||||
return isBlocklisted
|
|
||||||
} catch {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.edit-license.title',
|
|
||||||
defaultMessage: 'Edit license',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gpl-license-source-required',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.gpl-license-source-required.title',
|
|
||||||
defaultMessage: 'Provide source code',
|
|
||||||
}),
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
|
|
||||||
return formatMessage(
|
|
||||||
defineMessage({
|
|
||||||
id: 'nags.gpl-license-source-required.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Your {projectType} uses a license which requires source code to be available. Please provide a source code link or sources file for each additional version, or consider using a different license.',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const gplLicenses = [
|
|
||||||
'GPL-2.0',
|
|
||||||
'GPL-2.0+',
|
|
||||||
'GPL-2.0-only',
|
|
||||||
'GPL-2.0-or-later',
|
|
||||||
'GPL-3.0',
|
|
||||||
'GPL-3.0+',
|
|
||||||
'GPL-3.0-only',
|
|
||||||
'GPL-3.0-or-later',
|
|
||||||
'LGPL-2.1',
|
|
||||||
'LGPL-2.1+',
|
|
||||||
'LGPL-2.1-only',
|
|
||||||
'LGPL-2.1-or-later',
|
|
||||||
'LGPL-3.0',
|
|
||||||
'LGPL-3.0+',
|
|
||||||
'LGPL-3.0-only',
|
|
||||||
'LGPL-3.0-or-later',
|
|
||||||
'AGPL-3.0',
|
|
||||||
'AGPL-3.0+',
|
|
||||||
'AGPL-3.0-only',
|
|
||||||
'AGPL-3.0-or-later',
|
|
||||||
'MPL-2.0',
|
|
||||||
]
|
|
||||||
|
|
||||||
const isGplLicense = gplLicenses.includes(context.project.license.id)
|
|
||||||
const hasSourceUrl = !!context.project.source_url
|
|
||||||
const hasAdditionalFiles = (context: NagContext) => {
|
|
||||||
let hasAdditional = true
|
|
||||||
context.versions.forEach((version) => {
|
|
||||||
if (version.files.length < 2) hasAdditional = false
|
|
||||||
})
|
|
||||||
return hasAdditional
|
|
||||||
}
|
|
||||||
const notSourceAsDistributed = (context: NagContext) =>
|
|
||||||
context.project.project_type === 'mod' || context.project.project_type === 'plugin'
|
|
||||||
|
|
||||||
return (
|
|
||||||
isGplLicense &&
|
|
||||||
notSourceAsDistributed(context) &&
|
|
||||||
!hasSourceUrl &&
|
|
||||||
!hasAdditionalFiles(context)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/links',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.visit-links-settings.title',
|
|
||||||
defaultMessage: 'Visit links settings',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import type { Project } from '@modrinth/utils'
|
|
||||||
import type { Nag, NagContext } from '../../types/nags'
|
|
||||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
|
||||||
|
|
||||||
const allResolutionTags = ['8x-', '16x', '32x', '48x', '64x', '128x', '256x', '512x+']
|
|
||||||
|
|
||||||
const MAX_TAG_COUNT = 8
|
|
||||||
|
|
||||||
function getCategories(
|
|
||||||
project: Project & { actualProjectType: string },
|
|
||||||
tags: {
|
|
||||||
categories?: {
|
|
||||||
project_type: string
|
|
||||||
}[]
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
tags.categories?.filter(
|
|
||||||
(category: { project_type: string }) => category.project_type === project.actualProjectType,
|
|
||||||
) ?? []
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const tagsNags: Nag[] = [
|
|
||||||
{
|
|
||||||
id: 'too-many-tags',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.too-many-tags.title',
|
|
||||||
defaultMessage: 'Select accurate tags',
|
|
||||||
}),
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const tagCount =
|
|
||||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
|
||||||
const maxTagCount = MAX_TAG_COUNT
|
|
||||||
|
|
||||||
return formatMessage(
|
|
||||||
defineMessage({
|
|
||||||
id: 'nags.too-many-tags.description',
|
|
||||||
defaultMessage:
|
|
||||||
"You've selected {tagCount} tags. Consider reducing to {maxTagCount} or fewer to make sure your project appears in relevant search results.",
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
tagCount,
|
|
||||||
maxTagCount,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const tagCount =
|
|
||||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
|
||||||
return tagCount > MAX_TAG_COUNT
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/tags',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.edit-tags.title',
|
|
||||||
defaultMessage: 'Edit tags',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'multiple-resolution-tags',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.multiple-resolution-tags.title',
|
|
||||||
defaultMessage: 'Select correct resolution',
|
|
||||||
}),
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const resolutionTags = context.project.categories
|
|
||||||
.concat(context.project.additional_categories)
|
|
||||||
.filter((tag: string) => allResolutionTags.includes(tag))
|
|
||||||
|
|
||||||
const sortedTags = resolutionTags.toSorted((a, b) => {
|
|
||||||
return allResolutionTags.indexOf(a) - allResolutionTags.indexOf(b)
|
|
||||||
})
|
|
||||||
|
|
||||||
return formatMessage(
|
|
||||||
defineMessage({
|
|
||||||
id: 'nags.multiple-resolution-tags.description',
|
|
||||||
defaultMessage:
|
|
||||||
"You've selected {count} resolution tags ({tags}). Resource packs should typically only have one resolution tag that matches their primary resolution.",
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
count: resolutionTags.length,
|
|
||||||
tags: sortedTags
|
|
||||||
.join(', ')
|
|
||||||
.replace('8x-', '8x or lower')
|
|
||||||
.replace('512x+', '512x or higher'),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
if (context.project.project_type !== 'resourcepack') return false
|
|
||||||
|
|
||||||
const resolutionTags = context.project.categories
|
|
||||||
.concat(context.project.additional_categories)
|
|
||||||
.filter((tag: string) => allResolutionTags.includes(tag))
|
|
||||||
return resolutionTags.length > 1
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/tags',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.edit-tags.title',
|
|
||||||
defaultMessage: 'Edit tags',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'all-tags-selected',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.all-tags-selected.title',
|
|
||||||
defaultMessage: 'Select accurate tags',
|
|
||||||
}),
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const categoriesForProjectType = getCategories(
|
|
||||||
context.project as Project & { actualProjectType: string },
|
|
||||||
context.tags,
|
|
||||||
)
|
|
||||||
const totalAvailableTags = categoriesForProjectType.length
|
|
||||||
|
|
||||||
return formatMessage(
|
|
||||||
defineMessage({
|
|
||||||
id: 'nags.all-tags-selected.description',
|
|
||||||
defaultMessage:
|
|
||||||
"You've selected all {totalAvailableTags} available tags. This defeats the purpose of tags, which are meant to help users find relevant projects. Please select only the tags that are relevant to your project.",
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
totalAvailableTags,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const categoriesForProjectType = getCategories(
|
|
||||||
context.project as Project & { actualProjectType: string },
|
|
||||||
context.tags,
|
|
||||||
)
|
|
||||||
const totalSelectedTags =
|
|
||||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
|
||||||
return (
|
|
||||||
totalSelectedTags === categoriesForProjectType.length &&
|
|
||||||
context.project.project_type !== 'project'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/tags',
|
|
||||||
title: defineMessage({
|
|
||||||
id: 'nags.edit-tags.title',
|
|
||||||
defaultMessage: 'Edit tags',
|
|
||||||
}),
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -2,12 +2,9 @@ export * from './types/actions'
|
|||||||
export * from './types/messages'
|
export * from './types/messages'
|
||||||
export * from './types/stage'
|
export * from './types/stage'
|
||||||
export * from './types/keybinds'
|
export * from './types/keybinds'
|
||||||
export * from './types/nags'
|
|
||||||
export * from './types/reports'
|
export * from './types/reports'
|
||||||
export * from './utils'
|
export * from './utils'
|
||||||
|
|
||||||
export * from './data/nags/index'
|
|
||||||
export { default as nags } from './data/nags'
|
|
||||||
export { finalPermissionMessages } from './data/modpack-permissions-stage'
|
export { finalPermissionMessages } from './data/modpack-permissions-stage'
|
||||||
export { default as checklist } from './data/checklist'
|
export { default as checklist } from './data/checklist'
|
||||||
export { default as keybinds } from './data/keybinds'
|
export { default as keybinds } from './data/keybinds'
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
{
|
|
||||||
"nags.add-description.description": {
|
|
||||||
"defaultMessage": "A description that clearly describes the project's purpose and function is required."
|
|
||||||
},
|
|
||||||
"nags.add-description.title": {
|
|
||||||
"defaultMessage": "Add a description"
|
|
||||||
},
|
|
||||||
"nags.add-icon.description": {
|
|
||||||
"defaultMessage": "Adding a unique, relevant, and engaging icon makes your project identifiable and helps it stand out."
|
|
||||||
},
|
|
||||||
"nags.add-icon.title": {
|
|
||||||
"defaultMessage": "Add an icon"
|
|
||||||
},
|
|
||||||
"nags.add-links.description": {
|
|
||||||
"defaultMessage": "Add any relevant links targeted outside of Modrinth, such as source code, an issue tracker, or a Discord invite."
|
|
||||||
},
|
|
||||||
"nags.add-links.title": {
|
|
||||||
"defaultMessage": "Add external links"
|
|
||||||
},
|
|
||||||
"nags.all-tags-selected.description": {
|
|
||||||
"defaultMessage": "You've selected all {totalAvailableTags} available tags. This defeats the purpose of tags, which are meant to help users find relevant projects. Please select only the tags that are relevant to your project."
|
|
||||||
},
|
|
||||||
"nags.all-tags-selected.title": {
|
|
||||||
"defaultMessage": "Select accurate tags"
|
|
||||||
},
|
|
||||||
"nags.description-too-short.description": {
|
|
||||||
"defaultMessage": "Your description is {length} readable characters. At least {minChars} characters is recommended to create a clear and informative description."
|
|
||||||
},
|
|
||||||
"nags.description-too-short.title": {
|
|
||||||
"defaultMessage": "Expand the description"
|
|
||||||
},
|
|
||||||
"nags.edit-description.title": {
|
|
||||||
"defaultMessage": "Edit description"
|
|
||||||
},
|
|
||||||
"nags.edit-license.title": {
|
|
||||||
"defaultMessage": "Edit license"
|
|
||||||
},
|
|
||||||
"nags.edit-summary.title": {
|
|
||||||
"defaultMessage": "Edit summary"
|
|
||||||
},
|
|
||||||
"nags.edit-tags.title": {
|
|
||||||
"defaultMessage": "Edit tags"
|
|
||||||
},
|
|
||||||
"nags.edit-title.title": {
|
|
||||||
"defaultMessage": "Edit title"
|
|
||||||
},
|
|
||||||
"nags.feature-gallery-image.description": {
|
|
||||||
"defaultMessage": "The featured gallery image is often how your project makes its first impression."
|
|
||||||
},
|
|
||||||
"nags.feature-gallery-image.title": {
|
|
||||||
"defaultMessage": "Feature a gallery image"
|
|
||||||
},
|
|
||||||
"nags.gallery.title": {
|
|
||||||
"defaultMessage": "Visit gallery page"
|
|
||||||
},
|
|
||||||
"nags.gpl-license-source-required.description": {
|
|
||||||
"defaultMessage": "Your {projectType} uses a license which requires source code to be available. Please provide a source code link or sources file for each additional version, or consider using a different license."
|
|
||||||
},
|
|
||||||
"nags.gpl-license-source-required.title": {
|
|
||||||
"defaultMessage": "Provide source code"
|
|
||||||
},
|
|
||||||
"nags.image-heavy-description.description": {
|
|
||||||
"defaultMessage": "Your Description should contain sufficient plain text or image alt-text, keeping it accessible to those using screen readers or with slow internet connections."
|
|
||||||
},
|
|
||||||
"nags.image-heavy-description.title": {
|
|
||||||
"defaultMessage": "Ensure accessibility"
|
|
||||||
},
|
|
||||||
"nags.invalid-license-url.description.default": {
|
|
||||||
"defaultMessage": "License URL is invalid."
|
|
||||||
},
|
|
||||||
"nags.invalid-license-url.description.domain": {
|
|
||||||
"defaultMessage": "Your license URL points to {domain}, which is not appropriate for license information. License URLs should link directly to your license file, not social media, gaming platforms, etc."
|
|
||||||
},
|
|
||||||
"nags.invalid-license-url.description.malformed": {
|
|
||||||
"defaultMessage": "Your license URL appears to be malformed. Please provide a valid URL to your license text."
|
|
||||||
},
|
|
||||||
"nags.invalid-license-url.title": {
|
|
||||||
"defaultMessage": "Add a valid license link"
|
|
||||||
},
|
|
||||||
"nags.link-shortener-usage.description": {
|
|
||||||
"defaultMessage": "Use of link shorteners or other methods to obscure where a link may lead in your external links or license link is prohibited, please only use appropriate full length links."
|
|
||||||
},
|
|
||||||
"nags.link-shortener-usage.title": {
|
|
||||||
"defaultMessage": "Don't use link shorteners"
|
|
||||||
},
|
|
||||||
"nags.long-headers.description": {
|
|
||||||
"defaultMessage": "{count, plural, one {# header} other {# headers}} in your description {count, plural, one {is} other {are}} too long. Headers should be concise and act as section titles, not full sentences."
|
|
||||||
},
|
|
||||||
"nags.long-headers.title": {
|
|
||||||
"defaultMessage": "Shorten headers"
|
|
||||||
},
|
|
||||||
"nags.minecraft-title-clause.description": {
|
|
||||||
"defaultMessage": "Projects must not use Minecraft's branding or include \"Minecraft\" as a significant part of the name."
|
|
||||||
},
|
|
||||||
"nags.minecraft-title-clause.title": {
|
|
||||||
"defaultMessage": "Avoid brand infringement"
|
|
||||||
},
|
|
||||||
"nags.missing-alt-text.description": {
|
|
||||||
"defaultMessage": "Some of your images are missing alt text, which is important for accessibility, especially for visually impaired users."
|
|
||||||
},
|
|
||||||
"nags.missing-alt-text.title": {
|
|
||||||
"defaultMessage": "Add image alt text"
|
|
||||||
},
|
|
||||||
"nags.misused-discord-link-description": {
|
|
||||||
"defaultMessage": "Discord invites can not be used for other link types. Please put your Discord link in the Discord Invite link field only."
|
|
||||||
},
|
|
||||||
"nags.misused-discord-link.title": {
|
|
||||||
"defaultMessage": "Move Discord invite"
|
|
||||||
},
|
|
||||||
"nags.moderation.title": {
|
|
||||||
"defaultMessage": "Visit moderation thread"
|
|
||||||
},
|
|
||||||
"nags.moderator-feedback.description": {
|
|
||||||
"defaultMessage": "Review and address all concerns from the moderation team before resubmitting."
|
|
||||||
},
|
|
||||||
"nags.moderator-feedback.title": {
|
|
||||||
"defaultMessage": "Review moderator feedback"
|
|
||||||
},
|
|
||||||
"nags.multiple-resolution-tags.description": {
|
|
||||||
"defaultMessage": "You've selected {count} resolution tags ({tags}). Resource packs should typically only have one resolution tag that matches their primary resolution."
|
|
||||||
},
|
|
||||||
"nags.multiple-resolution-tags.title": {
|
|
||||||
"defaultMessage": "Select correct resolution"
|
|
||||||
},
|
|
||||||
"nags.select-environments.description": {
|
|
||||||
"defaultMessage": "Select if the {projectType} functions on the client-side and/or server-side."
|
|
||||||
},
|
|
||||||
"nags.select-environments.title": {
|
|
||||||
"defaultMessage": "Select supported environments"
|
|
||||||
},
|
|
||||||
"nags.select-license.description": {
|
|
||||||
"defaultMessage": "Select the license your {projectType} is distributed under."
|
|
||||||
},
|
|
||||||
"nags.select-license.title": {
|
|
||||||
"defaultMessage": "Select a license"
|
|
||||||
},
|
|
||||||
"nags.select-tags.description": {
|
|
||||||
"defaultMessage": "Select the tags that correctly apply to your project to help the right users find it."
|
|
||||||
},
|
|
||||||
"nags.select-tags.title": {
|
|
||||||
"defaultMessage": "Select tags"
|
|
||||||
},
|
|
||||||
"nags.settings.description.title": {
|
|
||||||
"defaultMessage": "Visit description settings"
|
|
||||||
},
|
|
||||||
"nags.settings.environments.title": {
|
|
||||||
"defaultMessage": "Visit general settings"
|
|
||||||
},
|
|
||||||
"nags.settings.license.title": {
|
|
||||||
"defaultMessage": "Visit license settings"
|
|
||||||
},
|
|
||||||
"nags.settings.links.title": {
|
|
||||||
"defaultMessage": "Visit links settings"
|
|
||||||
},
|
|
||||||
"nags.settings.tags.title": {
|
|
||||||
"defaultMessage": "Visit tag settings"
|
|
||||||
},
|
|
||||||
"nags.settings.title": {
|
|
||||||
"defaultMessage": "Visit general settings"
|
|
||||||
},
|
|
||||||
"nags.summary-same-as-title.description": {
|
|
||||||
"defaultMessage": "Your summary can not be the same as your project's Name. It's important to create an informative and enticing Summary."
|
|
||||||
},
|
|
||||||
"nags.summary-same-as-title.title": {
|
|
||||||
"defaultMessage": "Make the summary unique"
|
|
||||||
},
|
|
||||||
"nags.summary-too-short.description": {
|
|
||||||
"defaultMessage": "Your summary is {length} characters. At least {minChars} characters is recommended to create an informative and enticing summary."
|
|
||||||
},
|
|
||||||
"nags.summary-too-short.title": {
|
|
||||||
"defaultMessage": "Expand the summary"
|
|
||||||
},
|
|
||||||
"nags.title-contains-technical-info.description": {
|
|
||||||
"defaultMessage": "Keeping your project's Name clean and makes it memorable easier to find. Version and loader information is automatically displayed alongside your project."
|
|
||||||
},
|
|
||||||
"nags.title-contains-technical-info.title": {
|
|
||||||
"defaultMessage": "Clean up the name"
|
|
||||||
},
|
|
||||||
"nags.too-many-tags.description": {
|
|
||||||
"defaultMessage": "You've selected {tagCount} tags. Consider reducing to {maxTagCount} or fewer to make sure your project appears in relevant search results."
|
|
||||||
},
|
|
||||||
"nags.too-many-tags.title": {
|
|
||||||
"defaultMessage": "Select accurate tags"
|
|
||||||
},
|
|
||||||
"nags.upload-version.description": {
|
|
||||||
"defaultMessage": "At least one version is required for a project to be submitted for review."
|
|
||||||
},
|
|
||||||
"nags.upload-version.title": {
|
|
||||||
"defaultMessage": "Upload a version"
|
|
||||||
},
|
|
||||||
"nags.verify-external-links.description": {
|
|
||||||
"defaultMessage": "Some of your external links may be using domains that are inappropriate for that type of link."
|
|
||||||
},
|
|
||||||
"nags.verify-external-links.title": {
|
|
||||||
"defaultMessage": "Verify external links"
|
|
||||||
},
|
|
||||||
"nags.versions.title": {
|
|
||||||
"defaultMessage": "Visit versions page"
|
|
||||||
},
|
|
||||||
"nags.visit-links-settings.title": {
|
|
||||||
"defaultMessage": "Visit links settings"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,17 +6,14 @@
|
|||||||
"types": "./index.d.ts",
|
"types": "./index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fix": "eslint . --fix && prettier --write . && pnpm run intl:extract",
|
"fix": "eslint . --fix && prettier --write ."
|
||||||
"intl:extract": "formatjs extract \"**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore \"node_modules/**/*\" --out-file locales/en-US/index.json --preserve-whitespace"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modrinth/assets": "workspace:*",
|
|
||||||
"@modrinth/utils": "workspace:*",
|
"@modrinth/utils": "workspace:*",
|
||||||
|
"@modrinth/assets": "workspace:*",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formatjs/cli": "^6.2.12",
|
|
||||||
"@vintl/vintl": "^4.4.1",
|
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-custom": "workspace:*",
|
"eslint-config-custom": "workspace:*",
|
||||||
"tsconfig": "workspace:*"
|
"tsconfig": "workspace:*"
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
import type { Project, User, Version } from '@modrinth/utils'
|
|
||||||
import type { MessageDescriptor } from '@vintl/vintl'
|
|
||||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type which represents the status type of a nag.
|
|
||||||
*
|
|
||||||
* - `required` indicates that the nag must be addressed.
|
|
||||||
* - `warning` indicates that the nag is important but not critical, and can be ignored. It is often used for issues that should be resolved but do not block project submission.
|
|
||||||
* - `suggestion` indicates that the nag is a recommendation and can be ignored.
|
|
||||||
*/
|
|
||||||
export type NagStatus = 'required' | 'warning' | 'suggestion' | 'special-submit-action'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface representing the context in which a nag is displayed.
|
|
||||||
* It includes the project, versions, current member, all members, and the current route.
|
|
||||||
* This context is used to determine whether a nag or it's link should be shown and how it should be presented.
|
|
||||||
*/
|
|
||||||
export interface NagContext {
|
|
||||||
/**
|
|
||||||
* The project associated with the nag.
|
|
||||||
*/
|
|
||||||
project: Project
|
|
||||||
/**
|
|
||||||
* The versions associated with the project.
|
|
||||||
*/
|
|
||||||
versions: Version[]
|
|
||||||
/**
|
|
||||||
* The current project member viewing the nag.
|
|
||||||
*/
|
|
||||||
currentMember: User
|
|
||||||
/**
|
|
||||||
* The current route in the application.
|
|
||||||
*/
|
|
||||||
currentRoute: string
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
tags: any
|
|
||||||
submitProject: (...any: any) => any
|
|
||||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface representing a nag's link.
|
|
||||||
*/
|
|
||||||
export interface NagLink {
|
|
||||||
/**
|
|
||||||
* A relative path to the nag's link, e.g. '/settings'.
|
|
||||||
*/
|
|
||||||
path: string
|
|
||||||
/**
|
|
||||||
* The text to display for the nag's link.
|
|
||||||
*/
|
|
||||||
title: MessageDescriptor | string
|
|
||||||
/**
|
|
||||||
* The status of the nag, which can be 'required', 'warning', or 'suggestion'.
|
|
||||||
*/
|
|
||||||
shouldShow?: (context: NagContext) => boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface representing a nag.
|
|
||||||
*/
|
|
||||||
export interface Nag {
|
|
||||||
/**
|
|
||||||
* A unique identifier for the nag.
|
|
||||||
*/
|
|
||||||
id: string
|
|
||||||
/**
|
|
||||||
* The title of the nag.
|
|
||||||
*/
|
|
||||||
title: MessageDescriptor | string
|
|
||||||
/**
|
|
||||||
* A function that returns the description of the nag.
|
|
||||||
* It can accept a context to provide dynamic descriptions.
|
|
||||||
*/
|
|
||||||
description: MessageDescriptor | ((context: NagContext) => string)
|
|
||||||
/**
|
|
||||||
* The status of the nag, which can be 'required', 'warning', or 'suggestion'.
|
|
||||||
*/
|
|
||||||
status: NagStatus
|
|
||||||
/**
|
|
||||||
* An optional icon for the nag, usually from `@modrinth/assets`.
|
|
||||||
* If not specified it will use the default icon associated with the nag status.
|
|
||||||
*/
|
|
||||||
icon?: FunctionalComponent<SVGAttributes>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A function that determines whether the nag should be shown based on the context.
|
|
||||||
*/
|
|
||||||
shouldShow: (context: NagContext) => boolean
|
|
||||||
/**
|
|
||||||
* An optional link associated with the nag.
|
|
||||||
* If provided, it should be displayed alongside the nag.
|
|
||||||
*/
|
|
||||||
link?: NagLink
|
|
||||||
}
|
|
||||||
+5
-12
@@ -18,14 +18,7 @@ export type DonationPlatform =
|
|||||||
| { short: 'ko-fi'; name: 'Ko-fi' }
|
| { short: 'ko-fi'; name: 'Ko-fi' }
|
||||||
| { short: 'other'; name: 'Other' }
|
| { short: 'other'; name: 'Other' }
|
||||||
|
|
||||||
export type ProjectType =
|
export type ProjectType = 'mod' | 'modpack' | 'resourcepack' | 'shader' | 'plugin' | 'datapack'
|
||||||
| 'mod'
|
|
||||||
| 'modpack'
|
|
||||||
| 'resourcepack'
|
|
||||||
| 'shader'
|
|
||||||
| 'plugin'
|
|
||||||
| 'datapack'
|
|
||||||
| 'project'
|
|
||||||
export type MonetizationStatus = 'monetized' | 'demonetized' | 'force-demonetized'
|
export type MonetizationStatus = 'monetized' | 'demonetized' | 'force-demonetized'
|
||||||
|
|
||||||
export type GameVersion = string
|
export type GameVersion = string
|
||||||
@@ -77,10 +70,10 @@ export interface Project {
|
|||||||
thread_id: ModrinthId
|
thread_id: ModrinthId
|
||||||
organization: ModrinthId
|
organization: ModrinthId
|
||||||
|
|
||||||
issues_url: string | null
|
issues_url?: string
|
||||||
source_url: string | null
|
source_url?: string
|
||||||
wiki_url: string | null
|
wiki_url?: string
|
||||||
discord_url: string | null
|
discord_url?: string
|
||||||
donation_urls: DonationLink<DonationPlatform>[]
|
donation_urls: DonationLink<DonationPlatform>[]
|
||||||
|
|
||||||
published: string
|
published: string
|
||||||
|
|||||||
Generated
+5
-43
@@ -479,12 +479,6 @@ importers:
|
|||||||
specifier: ^3.5.13
|
specifier: ^3.5.13
|
||||||
version: 3.5.13(typescript@5.8.3)
|
version: 3.5.13(typescript@5.8.3)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@formatjs/cli':
|
|
||||||
specifier: ^6.2.12
|
|
||||||
version: 6.2.12(@vue/compiler-core@3.5.13)(vue@3.5.13(typescript@5.8.3))
|
|
||||||
'@vintl/vintl':
|
|
||||||
specifier: ^4.4.1
|
|
||||||
version: 4.4.1(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
|
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.57.0
|
specifier: ^8.57.0
|
||||||
version: 8.57.0
|
version: 8.57.0
|
||||||
@@ -4178,8 +4172,8 @@ packages:
|
|||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.182:
|
electron-to-chromium@1.5.191:
|
||||||
resolution: {integrity: sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==}
|
resolution: {integrity: sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.71:
|
electron-to-chromium@1.5.71:
|
||||||
resolution: {integrity: sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA==}
|
resolution: {integrity: sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA==}
|
||||||
@@ -8969,10 +8963,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.13(typescript@5.5.4)
|
vue: 3.5.13(typescript@5.5.4)
|
||||||
|
|
||||||
'@braw/async-computed@5.0.2(vue@3.5.13(typescript@5.8.3))':
|
|
||||||
dependencies:
|
|
||||||
vue: 3.5.13(typescript@5.8.3)
|
|
||||||
|
|
||||||
'@cloudflare/kv-asset-handler@0.3.4':
|
'@cloudflare/kv-asset-handler@0.3.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
mime: 3.0.0
|
mime: 3.0.0
|
||||||
@@ -9500,11 +9490,6 @@ snapshots:
|
|||||||
'@vue/compiler-core': 3.5.13
|
'@vue/compiler-core': 3.5.13
|
||||||
vue: 3.5.13(typescript@5.5.4)
|
vue: 3.5.13(typescript@5.5.4)
|
||||||
|
|
||||||
'@formatjs/cli@6.2.12(@vue/compiler-core@3.5.13)(vue@3.5.13(typescript@5.8.3))':
|
|
||||||
optionalDependencies:
|
|
||||||
'@vue/compiler-core': 3.5.13
|
|
||||||
vue: 3.5.13(typescript@5.8.3)
|
|
||||||
|
|
||||||
'@formatjs/ecma402-abstract@1.18.3':
|
'@formatjs/ecma402-abstract@1.18.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/intl-localematcher': 0.5.4
|
'@formatjs/intl-localematcher': 0.5.4
|
||||||
@@ -9562,18 +9547,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.5.4
|
typescript: 5.5.4
|
||||||
|
|
||||||
'@formatjs/intl@2.10.4(typescript@5.8.3)':
|
|
||||||
dependencies:
|
|
||||||
'@formatjs/ecma402-abstract': 2.0.0
|
|
||||||
'@formatjs/fast-memoize': 2.2.0
|
|
||||||
'@formatjs/icu-messageformat-parser': 2.7.8
|
|
||||||
'@formatjs/intl-displaynames': 6.6.8
|
|
||||||
'@formatjs/intl-listformat': 7.5.7
|
|
||||||
intl-messageformat: 10.5.14
|
|
||||||
tslib: 2.6.3
|
|
||||||
optionalDependencies:
|
|
||||||
typescript: 5.8.3
|
|
||||||
|
|
||||||
'@formatjs/ts-transformer@3.13.14':
|
'@formatjs/ts-transformer@3.13.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/icu-messageformat-parser': 2.7.8
|
'@formatjs/icu-messageformat-parser': 2.7.8
|
||||||
@@ -11261,17 +11234,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@vintl/vintl@4.4.1(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))':
|
|
||||||
dependencies:
|
|
||||||
'@braw/async-computed': 5.0.2(vue@3.5.13(typescript@5.8.3))
|
|
||||||
'@formatjs/icu-messageformat-parser': 2.7.8
|
|
||||||
'@formatjs/intl': 2.10.4(typescript@5.8.3)
|
|
||||||
'@formatjs/intl-localematcher': 0.4.2
|
|
||||||
intl-messageformat: 10.5.14
|
|
||||||
vue: 3.5.13(typescript@5.8.3)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- typescript
|
|
||||||
|
|
||||||
'@vitejs/plugin-vue-jsx@4.1.1(vite@5.4.11(@types/node@20.14.11)(sass@1.77.6)(terser@5.43.1))(vue@3.5.13(typescript@5.5.4))':
|
'@vitejs/plugin-vue-jsx@4.1.1(vite@5.4.11(@types/node@20.14.11)(sass@1.77.6)(terser@5.43.1))(vue@3.5.13(typescript@5.5.4))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.26.0
|
'@babel/core': 7.26.0
|
||||||
@@ -12077,7 +12039,7 @@ snapshots:
|
|||||||
browserslist@4.25.1:
|
browserslist@4.25.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
caniuse-lite: 1.0.30001727
|
caniuse-lite: 1.0.30001727
|
||||||
electron-to-chromium: 1.5.182
|
electron-to-chromium: 1.5.191
|
||||||
node-releases: 2.0.19
|
node-releases: 2.0.19
|
||||||
update-browserslist-db: 1.1.3(browserslist@4.25.1)
|
update-browserslist-db: 1.1.3(browserslist@4.25.1)
|
||||||
optional: true
|
optional: true
|
||||||
@@ -12655,7 +12617,7 @@ snapshots:
|
|||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.182:
|
electron-to-chromium@1.5.191:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
electron-to-chromium@1.5.71: {}
|
electron-to-chromium@1.5.71: {}
|
||||||
@@ -17414,7 +17376,7 @@ snapshots:
|
|||||||
magic-string: 0.30.17
|
magic-string: 0.30.17
|
||||||
mlly: 1.7.4
|
mlly: 1.7.4
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
picomatch: 4.0.2
|
picomatch: 4.0.3
|
||||||
pkg-types: 2.2.0
|
pkg-types: 2.2.0
|
||||||
scule: 1.3.0
|
scule: 1.3.0
|
||||||
strip-literal: 3.0.0
|
strip-literal: 3.0.0
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.88.0"
|
channel = "1.89.0"
|
||||||
Reference in new issue
Block a user