Compare commits
18 Commits
josiah/rus
...
fetch/offe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8546efd572 | ||
|
|
2796d770f7 | ||
|
|
6d39122ca9 | ||
|
|
09e89724de | ||
|
|
7a39f5853f | ||
|
|
e8639510aa | ||
|
|
d6ee0c42c8 | ||
|
|
c1fc072efe | ||
|
|
3f36a67bc8 | ||
|
|
b0443dc49d | ||
|
|
4981151cea | ||
|
|
360d24f2e0 | ||
|
|
84cfd21920 | ||
|
|
158f5171fc | ||
|
|
1fd21e99c3 | ||
|
|
da0fed3e21 | ||
|
|
b65a16adff | ||
|
|
6909f4a678 |
1476
Cargo.lock
generated
1476
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
72
Cargo.toml
72
Cargo.toml
@@ -25,31 +25,31 @@ actix-ws = "0.3.0"
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
ariadne = { path = "packages/ariadne" }
|
||||
async_zip = "0.0.17"
|
||||
async-compression = { version = "0.4.27", default-features = false }
|
||||
async-compression = { version = "0.4.25", default-features = false }
|
||||
async-recursion = "1.1.1"
|
||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||
"runtime-tokio-hyper-rustls",
|
||||
] }
|
||||
async-trait = "0.1.88"
|
||||
async-tungstenite = { version = "0.30.0", default-features = false, features = [
|
||||
async-tungstenite = { version = "0.29.1", default-features = false, features = [
|
||||
"futures-03-sink",
|
||||
] }
|
||||
async-walkdir = "2.1.0"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.9.1"
|
||||
bytemuck = "1.23.1"
|
||||
bytemuck = "1.23.0"
|
||||
bytes = "1.10.1"
|
||||
censor = "0.3.0"
|
||||
chardetng = "0.1.17"
|
||||
chrono = "0.4.41"
|
||||
clap = "4.5.43"
|
||||
clap = "4.5.40"
|
||||
clickhouse = "0.13.3"
|
||||
color-thief = "0.2.2"
|
||||
console-subscriber = "0.4.1"
|
||||
daedalus = { path = "packages/daedalus" }
|
||||
dashmap = "6.1.0"
|
||||
data-url = "0.3.1"
|
||||
deadpool-redis = "0.22.0"
|
||||
deadpool-redis = "0.21.1"
|
||||
dirs = "6.0.0"
|
||||
discord-rich-presence = "0.2.5"
|
||||
dotenv-build = "0.1.1"
|
||||
@@ -57,7 +57,7 @@ dotenvy = "0.15.7"
|
||||
dunce = "1.0.5"
|
||||
either = "1.15.0"
|
||||
encoding_rs = "0.8.35"
|
||||
enumset = "1.1.7"
|
||||
enumset = "1.1.6"
|
||||
flate2 = "1.1.2"
|
||||
fs4 = { version = "0.13.1", 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",
|
||||
"tls12",
|
||||
] }
|
||||
hyper-util = "0.1.16"
|
||||
hyper-util = "0.1.14"
|
||||
iana-time-zone = "0.1.63"
|
||||
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
||||
indexmap = "2.10.0"
|
||||
indicatif = "0.18.0"
|
||||
indexmap = "2.9.0"
|
||||
indicatif = "0.17.11"
|
||||
itertools = "0.14.0"
|
||||
jemalloc_pprof = "0.8.1"
|
||||
jemalloc_pprof = "0.7.0"
|
||||
json-patch = { version = "4.0.0", default-features = false }
|
||||
lettre = { version = "0.11.18", default-features = false, features = [
|
||||
lettre = { version = "0.11.17", default-features = false, features = [
|
||||
"builder",
|
||||
"hostname",
|
||||
"pool",
|
||||
@@ -92,24 +92,24 @@ lettre = { version = "0.11.18", default-features = false, features = [
|
||||
"smtp-transport",
|
||||
] }
|
||||
maxminddb = "0.26.0"
|
||||
meilisearch-sdk = { version = "0.29.1", default-features = false }
|
||||
meilisearch-sdk = { version = "0.28.0", default-features = false }
|
||||
murmur2 = "0.1.0"
|
||||
native-dialog = "0.9.0"
|
||||
notify = { version = "8.2.0", default-features = false }
|
||||
notify-debouncer-mini = { version = "0.7.0", default-features = false }
|
||||
notify = { version = "8.0.0", default-features = false }
|
||||
notify-debouncer-mini = { version = "0.6.0", default-features = false }
|
||||
p256 = "0.13.2"
|
||||
paste = "1.0.15"
|
||||
phf = { version = "0.12.1", features = ["macros"] }
|
||||
png = "0.17.16"
|
||||
prometheus = "0.14.0"
|
||||
quartz_nbt = "0.2.9"
|
||||
quick-xml = "0.38.1"
|
||||
quick-xml = "0.37.5"
|
||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||
redis = "0.32.4"
|
||||
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.22", default-features = false }
|
||||
rgb = "0.8.52"
|
||||
reqwest = { version = "0.12.20", default-features = false }
|
||||
rgb = "0.8.50"
|
||||
rust_decimal = { version = "1.37.2", features = [
|
||||
"serde-with-float",
|
||||
"serde-with-str",
|
||||
@@ -121,7 +121,7 @@ rust-s3 = { version = "0.35.1", default-features = false, features = [
|
||||
"tokio-rustls-tls",
|
||||
] }
|
||||
rusty-money = "0.4.1"
|
||||
sentry = { version = "0.42.0", default-features = false, features = [
|
||||
sentry = { version = "0.41.0", default-features = false, features = [
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"debug-images",
|
||||
@@ -129,45 +129,45 @@ sentry = { version = "0.42.0", default-features = false, features = [
|
||||
"reqwest",
|
||||
"rustls",
|
||||
] }
|
||||
sentry-actix = "0.42.0"
|
||||
sentry-actix = "0.41.0"
|
||||
serde = "1.0.219"
|
||||
serde_bytes = "0.11.17"
|
||||
serde_cbor = "0.11.2"
|
||||
serde_ini = "0.2.0"
|
||||
serde_json = "1.0.142"
|
||||
serde_with = "3.14.0"
|
||||
serde_json = "1.0.140"
|
||||
serde_with = "3.13.0"
|
||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||
sha1 = "0.10.6"
|
||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||
sha2 = "0.10.9"
|
||||
spdx = "0.10.9"
|
||||
spdx = "0.10.8"
|
||||
sqlx = { version = "0.8.6", default-features = false }
|
||||
sysinfo = { version = "0.36.1", default-features = false }
|
||||
sysinfo = { version = "0.35.2", default-features = false }
|
||||
tar = "0.4.44"
|
||||
tauri = "2.7.0"
|
||||
tauri-build = "2.3.1"
|
||||
tauri-plugin-deep-link = "2.4.1"
|
||||
tauri-plugin-dialog = "2.3.2"
|
||||
tauri-plugin-http = "2.5.1"
|
||||
tauri = "2.6.1"
|
||||
tauri-build = "2.3.0"
|
||||
tauri-plugin-deep-link = "2.4.0"
|
||||
tauri-plugin-dialog = "2.3.0"
|
||||
tauri-plugin-http = "2.5.0"
|
||||
tauri-plugin-opener = "2.4.0"
|
||||
tauri-plugin-os = "2.3.0"
|
||||
tauri-plugin-single-instance = "2.3.2"
|
||||
tauri-plugin-single-instance = "2.3.0"
|
||||
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
|
||||
"rustls-tls",
|
||||
"zip",
|
||||
] }
|
||||
tauri-plugin-window-state = "2.4.0"
|
||||
tauri-plugin-window-state = "2.3.0"
|
||||
tempfile = "3.20.0"
|
||||
theseus = { path = "packages/app-lib" }
|
||||
thiserror = "2.0.12"
|
||||
tikv-jemalloc-ctl = "0.6.0"
|
||||
tikv-jemallocator = "0.6.0"
|
||||
tokio = "1.47.1"
|
||||
tokio = "1.45.1"
|
||||
tokio-stream = "0.1.17"
|
||||
tokio-util = "0.7.16"
|
||||
tokio-util = "0.7.15"
|
||||
totp-rs = "5.7.0"
|
||||
tracing = "0.1.41"
|
||||
tracing-actix-web = "0.7.19"
|
||||
tracing-actix-web = "0.7.18"
|
||||
tracing-error = "0.2.1"
|
||||
tracing-subscriber = "0.3.19"
|
||||
url = "2.5.4"
|
||||
@@ -179,7 +179,7 @@ whoami = "1.6.0"
|
||||
winreg = "0.55.0"
|
||||
woothee = "0.13.0"
|
||||
yaserde = "0.12.0"
|
||||
zip = { version = "4.3.0", default-features = false, features = [
|
||||
zip = { version = "4.2.0", default-features = false, features = [
|
||||
"bzip2",
|
||||
"deflate",
|
||||
"deflate64",
|
||||
@@ -226,7 +226,7 @@ wildcard_dependencies = "warn"
|
||||
warnings = "deny"
|
||||
|
||||
[patch.crates-io]
|
||||
wry = { git = "https://github.com/modrinth/wry", rev = "f2ce0b0" }
|
||||
wry = { git = "https://github.com/modrinth/wry", rev = "21db186" }
|
||||
|
||||
# Optimize for speed and reduce size on release builds
|
||||
[profile.release]
|
||||
|
||||
@@ -197,13 +197,15 @@ pub async fn open_link<R: Runtime>(
|
||||
|
||||
if url::Url::parse(&path).is_ok()
|
||||
&& !state.malicious_origins.contains(&origin)
|
||||
&& let Some(last_click) = state.last_click
|
||||
&& last_click.elapsed() < Duration::from_millis(100)
|
||||
{
|
||||
let _ = app.opener().open_url(&path, None::<String>);
|
||||
state.last_click = None;
|
||||
if let Some(last_click) = state.last_click {
|
||||
if last_click.elapsed() < Duration::from_millis(100) {
|
||||
let _ = app.opener().open_url(&path, None::<String>);
|
||||
state.last_click = None;
|
||||
|
||||
return Ok(());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Malicious click: {path} origin {origin}");
|
||||
|
||||
@@ -59,13 +59,16 @@ pub async fn login<R: Runtime>(
|
||||
.url()?
|
||||
.as_str()
|
||||
.starts_with("https://login.live.com/oauth20_desktop.srf")
|
||||
&& let Some((_, code)) =
|
||||
window.url()?.query_pairs().find(|x| x.0 == "code")
|
||||
{
|
||||
window.close()?;
|
||||
let val = minecraft_auth::finish_login(&code.clone(), flow).await?;
|
||||
if let Some((_, code)) =
|
||||
window.url()?.query_pairs().find(|x| x.0 == "code")
|
||||
{
|
||||
window.close()?;
|
||||
let val =
|
||||
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;
|
||||
|
||||
@@ -63,11 +63,11 @@ 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
|
||||
if let tauri_plugin_os::Version::Semantic(major, minor, _) =
|
||||
tauri_plugin_os::version()
|
||||
&& major >= 12
|
||||
&& minor >= 3
|
||||
{
|
||||
// Mac os version is 12.3 or higher, we allow mouseover
|
||||
return false;
|
||||
if major >= 12 && minor >= 3 {
|
||||
// Mac os version is 12.3 or higher, we allow mouseover
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
} else {
|
||||
|
||||
@@ -233,10 +233,10 @@ fn main() {
|
||||
});
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
if let Some(window) = app.get_window("main")
|
||||
&& let Err(e) = window.set_shadow(true)
|
||||
{
|
||||
tracing::warn!("Failed to set window shadow: {e}");
|
||||
if let Some(window) = app.get_window("main") {
|
||||
if let Err(e) = window.set_shadow(true) {
|
||||
tracing::warn!("Failed to set window shadow: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -506,25 +506,27 @@ async fn fetch(
|
||||
|
||||
return Ok(lib);
|
||||
}
|
||||
} else if let Some(url) = &lib.url
|
||||
&& !url.is_empty()
|
||||
{
|
||||
insert_mirrored_artifact(
|
||||
&lib.name,
|
||||
None,
|
||||
vec![
|
||||
url.clone(),
|
||||
"https://libraries.minecraft.net/".to_string(),
|
||||
"https://maven.creeperhost.net/".to_string(),
|
||||
maven_url.to_string(),
|
||||
],
|
||||
false,
|
||||
mirror_artifacts,
|
||||
)?;
|
||||
} else if let Some(url) = &lib.url {
|
||||
if !url.is_empty() {
|
||||
insert_mirrored_artifact(
|
||||
&lib.name,
|
||||
None,
|
||||
vec![
|
||||
url.clone(),
|
||||
"https://libraries.minecraft.net/"
|
||||
.to_string(),
|
||||
"https://maven.creeperhost.net/"
|
||||
.to_string(),
|
||||
maven_url.to_string(),
|
||||
],
|
||||
false,
|
||||
mirror_artifacts,
|
||||
)?;
|
||||
|
||||
lib.url = Some(format_url("maven/"));
|
||||
lib.url = Some(format_url("maven/"));
|
||||
|
||||
return Ok(lib);
|
||||
return Ok(lib);
|
||||
}
|
||||
}
|
||||
|
||||
// Other libraries are generally available in the "maven" directory of the installer. If they are
|
||||
|
||||
@@ -93,22 +93,22 @@ async fn main() -> Result<()> {
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<bool>().ok())
|
||||
.unwrap_or(false)
|
||||
&& let Ok(token) = dotenvy::var("CLOUDFLARE_TOKEN")
|
||||
&& let Ok(zone_id) = dotenvy::var("CLOUDFLARE_ZONE_ID")
|
||||
{
|
||||
let cache_clears = upload_files
|
||||
.into_iter()
|
||||
.map(|x| format_url(&x.0))
|
||||
.chain(
|
||||
mirror_artifacts
|
||||
if let Ok(token) = dotenvy::var("CLOUDFLARE_TOKEN") {
|
||||
if let Ok(zone_id) = dotenvy::var("CLOUDFLARE_ZONE_ID") {
|
||||
let cache_clears = upload_files
|
||||
.into_iter()
|
||||
.map(|x| format_url(&format!("maven/{}", x.0))),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
.map(|x| format_url(&x.0))
|
||||
.chain(
|
||||
mirror_artifacts
|
||||
.into_iter()
|
||||
.map(|x| format_url(&format!("maven/{}", x.0))),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Cloudflare ratelimits cache clears to 500 files per request
|
||||
for chunk in cache_clears.chunks(500) {
|
||||
REQWEST_CLIENT.post(format!("https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache"))
|
||||
// Cloudflare ratelimits cache clears to 500 files per request
|
||||
for chunk in cache_clears.chunks(500) {
|
||||
REQWEST_CLIENT.post(format!("https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache"))
|
||||
.bearer_auth(&token)
|
||||
.json(&serde_json::json!({
|
||||
"files": chunk
|
||||
@@ -128,6 +128,8 @@ async fn main() -> Result<()> {
|
||||
item: "cloudflare clear cache".to_string(),
|
||||
}
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -167,18 +167,20 @@ pub async fn download_file(
|
||||
let bytes = x.bytes().await;
|
||||
|
||||
if let Ok(bytes) = bytes {
|
||||
if let Some(sha1) = sha1
|
||||
&& &*sha1_async(bytes.clone()).await? != sha1
|
||||
{
|
||||
if attempt <= 3 {
|
||||
continue;
|
||||
} else {
|
||||
return Err(crate::ErrorKind::ChecksumFailure {
|
||||
hash: sha1.to_string(),
|
||||
url: url.to_string(),
|
||||
tries: attempt,
|
||||
if let Some(sha1) = sha1 {
|
||||
if &*sha1_async(bytes.clone()).await? != sha1 {
|
||||
if attempt <= 3 {
|
||||
continue;
|
||||
} else {
|
||||
return Err(
|
||||
crate::ErrorKind::ChecksumFailure {
|
||||
hash: sha1.to_string(),
|
||||
url: url.to_string(),
|
||||
tries: attempt,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
34
apps/labrinth/.sqlx/query-139ba392ab53d975e63e2c328abad04831b4bed925bded054bb8a35d0680bed8.json
generated
Normal file
34
apps/labrinth/.sqlx/query-139ba392ab53d975e63e2c328abad04831b4bed925bded054bb8a35d0680bed8.json
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, metadata, unitary\n FROM products\n WHERE metadata ->> 'type' = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "metadata",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "unitary",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "139ba392ab53d975e63e2c328abad04831b4bed925bded054bb8a35d0680bed8"
|
||||
}
|
||||
15
apps/labrinth/.sqlx/query-1dea22589b0440cfeaf98b6869bdaad852d58c61cf2a1affb01acc4984d42341.json
generated
Normal file
15
apps/labrinth/.sqlx/query-1dea22589b0440cfeaf98b6869bdaad852d58c61cf2a1affb01acc4984d42341.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE users_redeemals\n SET status = $1\n WHERE\n status = $2\n AND NOW() - last_attempt > INTERVAL '5 minutes'\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1dea22589b0440cfeaf98b6869bdaad852d58c61cf2a1affb01acc4984d42341"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n id, user_id, price_id, amount, currency_code, status, due, last_attempt,\n charge_type, subscription_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n subscription_interval AS \"subscription_interval?\",\n payment_platform,\n payment_platform_id AS \"payment_platform_id?\",\n parent_charge_id AS \"parent_charge_id?\",\n net AS \"net?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled' OR status = 'failed')",
|
||||
"query": "\n SELECT\n id, user_id, price_id, amount, currency_code, status, due, last_attempt,\n charge_type, subscription_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n subscription_interval AS \"subscription_interval?\",\n payment_platform,\n payment_platform_id AS \"payment_platform_id?\",\n parent_charge_id AS \"parent_charge_id?\",\n net AS \"net?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -102,5 +102,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cf020daa52a1316e5f60d197196b880b72c0b2a576e470d9fd7182558103d055"
|
||||
"hash": "2e18682890f7ec5a618991c2a4c77ca9546970f314f902a5197eb2d189cf81f7"
|
||||
}
|
||||
59
apps/labrinth/.sqlx/query-58ccda393820a272d72a3e41eccc5db30ab6ad0bb346caf781efdb5aab524286.json
generated
Normal file
59
apps/labrinth/.sqlx/query-58ccda393820a272d72a3e41eccc5db30ab6ad0bb346caf781efdb5aab524286.json
generated
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT * FROM users_redeemals WHERE status = $1 LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "offer",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "redeemed",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "last_attempt",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "n_attempts",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "58ccda393820a272d72a3e41eccc5db30ab6ad0bb346caf781efdb5aab524286"
|
||||
}
|
||||
27
apps/labrinth/.sqlx/query-7adff98b270adc4a48e2c8a89a32ca1b83104102190597f4cda05e6f1c1e8f26.json
generated
Normal file
27
apps/labrinth/.sqlx/query-7adff98b270adc4a48e2c8a89a32ca1b83104102190597f4cda05e6f1c1e8f26.json
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO users_redeemals\n (user_id, offer, redeemed, status, last_attempt, n_attempts)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7adff98b270adc4a48e2c8a89a32ca1b83104102190597f4cda05e6f1c1e8f26"
|
||||
}
|
||||
19
apps/labrinth/.sqlx/query-8d61d1ecc5321e2ac8932ef99de0f77e49cced9c7726ea746392a5fcbe75f2f5.json
generated
Normal file
19
apps/labrinth/.sqlx/query-8d61d1ecc5321e2ac8932ef99de0f77e49cced9c7726ea746392a5fcbe75f2f5.json
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE users_redeemals\n SET\n offer = $2,\n status = $3,\n redeemed = $4,\n last_attempt = $5,\n n_attempts = $6\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8d61d1ecc5321e2ac8932ef99de0f77e49cced9c7726ea746392a5fcbe75f2f5"
|
||||
}
|
||||
29
apps/labrinth/.sqlx/query-949da1b1e3c772f79dd1248f99774fa39f140d3943f975067799f46f2cb48a0f.json
generated
Normal file
29
apps/labrinth/.sqlx/query-949da1b1e3c772f79dd1248f99774fa39f140d3943f975067799f46f2cb48a0f.json
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n users.id,\n users_redeemals.status AS \"status: Option<String>\"\n FROM\n users\n LEFT JOIN\n users_redeemals ON users_redeemals.user_id = users.id\n AND users_redeemals.offer = $2\n WHERE\n users.username = $1\n ORDER BY\n users_redeemals.redeemed DESC\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "status: Option<String>",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "949da1b1e3c772f79dd1248f99774fa39f140d3943f975067799f46f2cb48a0f"
|
||||
}
|
||||
23
apps/labrinth/.sqlx/query-9898e9962ba497ef8482ffa57d6590f7933e9f2465e9458fab005fe33d96ec7a.json
generated
Normal file
23
apps/labrinth/.sqlx/query-9898e9962ba497ef8482ffa57d6590f7933e9f2465e9458fab005fe33d96ec7a.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n EXISTS (\n SELECT\n 1\n FROM\n users_redeemals\n WHERE\n user_id = $1\n AND offer = $2\n ) AS \"exists!\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "9898e9962ba497ef8482ffa57d6590f7933e9f2465e9458fab005fe33d96ec7a"
|
||||
}
|
||||
41
apps/labrinth/.sqlx/query-c37fc91df7619ac5c10fd04fdc2556aa98b80ccbfc53813659464a0e5e09fae8.json
generated
Normal file
41
apps/labrinth/.sqlx/query-c37fc91df7619ac5c10fd04fdc2556aa98b80ccbfc53813659464a0e5e09fae8.json
generated
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, product_id, prices, currency_code\n FROM products_prices\n WHERE product_id = ANY($1::bigint[])\n AND public = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "product_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "prices",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "currency_code",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c37fc91df7619ac5c10fd04fdc2556aa98b80ccbfc53813659464a0e5e09fae8"
|
||||
}
|
||||
18
apps/labrinth/.sqlx/query-e3f6fa7e5ec6dee4fcdff904b3e692dccd55372d9cc827a1d68361fd036bc183.json
generated
Normal file
18
apps/labrinth/.sqlx/query-e3f6fa7e5ec6dee4fcdff904b3e692dccd55372d9cc827a1d68361fd036bc183.json
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE users_redeemals\n SET\n status = $3,\n last_attempt = $4,\n n_attempts = $5\n WHERE id = $1 AND status = $2\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e3f6fa7e5ec6dee4fcdff904b3e692dccd55372d9cc827a1d68361fd036bc183"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n id, user_id, price_id, amount, currency_code, status, due, last_attempt,\n charge_type, subscription_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n subscription_interval AS \"subscription_interval?\",\n payment_platform,\n payment_platform_id AS \"payment_platform_id?\",\n parent_charge_id AS \"parent_charge_id?\",\n net AS \"net?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n (\n (status = 'cancelled' AND due < NOW()) OR\n (status = 'failed' AND last_attempt < NOW() - INTERVAL '2 days')\n )\n ",
|
||||
"query": "\n SELECT\n id, user_id, price_id, amount, currency_code, status, due, last_attempt,\n charge_type, subscription_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n subscription_interval AS \"subscription_interval?\",\n payment_platform,\n payment_platform_id AS \"payment_platform_id?\",\n parent_charge_id AS \"parent_charge_id?\",\n net AS \"net?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n (\n (status = 'cancelled' AND due < NOW()) OR\n (status = 'expiring' AND due < NOW()) OR\n (status = 'failed' AND last_attempt < NOW() - INTERVAL '2 days')\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -102,5 +102,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bfcbcadda1e323d56b6a21fc060c56bff2f38a54cf65dd1cc21f209240c7091b"
|
||||
"hash": "fda7d5659efb2b6940a3247043945503c85e3f167216e0e2403e08095a3e32c9"
|
||||
}
|
||||
11
apps/labrinth/migrations/20250804221014_users-redeemals.sql
Normal file
11
apps/labrinth/migrations/20250804221014_users-redeemals.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Add migration script here
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users_redeemals (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
offer VARCHAR NOT NULL,
|
||||
redeemed TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
status VARCHAR NOT NULL,
|
||||
last_attempt TIMESTAMP WITH TIME ZONE,
|
||||
n_attempts INTEGER NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Add migration script here
|
||||
|
||||
ALTER TABLE
|
||||
products_prices
|
||||
ADD COLUMN
|
||||
public BOOLEAN NOT NULL DEFAULT true;
|
||||
@@ -322,11 +322,12 @@ pub async fn is_visible_collection(
|
||||
} else {
|
||||
!collection_data.status.is_hidden()
|
||||
}) && !collection_data.projects.is_empty();
|
||||
if let Some(user) = &user_option
|
||||
&& !authorized
|
||||
&& (user.role.is_mod() || user.id == collection_data.user_id.into())
|
||||
{
|
||||
authorized = true;
|
||||
if let Some(user) = &user_option {
|
||||
if !authorized
|
||||
&& (user.role.is_mod() || user.id == collection_data.user_id.into())
|
||||
{
|
||||
authorized = true;
|
||||
}
|
||||
}
|
||||
Ok(authorized)
|
||||
}
|
||||
@@ -355,10 +356,10 @@ pub async fn filter_visible_collections(
|
||||
|
||||
for collection in check_collections {
|
||||
// Collections are simple- if we are the owner or a mod, we can see it
|
||||
if let Some(user) = user_option
|
||||
&& (user.role.is_mod() || user.id == collection.user_id.into())
|
||||
{
|
||||
return_collections.push(collection.into());
|
||||
if let Some(user) = user_option {
|
||||
if user.role.is_mod() || user.id == collection.user_id.into() {
|
||||
return_collections.push(collection.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ impl DBCharge {
|
||||
) -> Result<Option<DBCharge>, DatabaseError> {
|
||||
let user_subscription_id = user_subscription_id.0;
|
||||
let res = select_charges_with_predicate!(
|
||||
"WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled' OR status = 'failed')",
|
||||
"WHERE subscription_id = $1 AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')",
|
||||
user_subscription_id
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
@@ -240,6 +240,7 @@ impl DBCharge {
|
||||
charge_type = $1 AND
|
||||
(
|
||||
(status = 'cancelled' AND due < NOW()) OR
|
||||
(status = 'expiring' AND due < NOW()) OR
|
||||
(status = 'failed' AND last_attempt < NOW() - INTERVAL '2 days')
|
||||
)
|
||||
"#,
|
||||
|
||||
@@ -95,10 +95,10 @@ impl DBFlow {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<DBFlow>, DatabaseError> {
|
||||
let flow = Self::get(id, redis).await?;
|
||||
if let Some(flow) = flow.as_ref()
|
||||
&& predicate(flow)
|
||||
{
|
||||
Self::remove(id, redis).await?;
|
||||
if let Some(flow) = flow.as_ref() {
|
||||
if predicate(flow) {
|
||||
Self::remove(id, redis).await?;
|
||||
}
|
||||
}
|
||||
Ok(flow)
|
||||
}
|
||||
|
||||
@@ -801,24 +801,24 @@ impl VersionField {
|
||||
};
|
||||
|
||||
if let Some(count) = countable {
|
||||
if let Some(min) = loader_field.min_val
|
||||
&& count < min
|
||||
{
|
||||
return Err(format!(
|
||||
"Provided value '{v}' for {field_name} is less than the minimum of {min}",
|
||||
v = serde_json::to_string(&value).unwrap_or_default(),
|
||||
field_name = loader_field.field,
|
||||
));
|
||||
if let Some(min) = loader_field.min_val {
|
||||
if count < min {
|
||||
return Err(format!(
|
||||
"Provided value '{v}' for {field_name} is less than the minimum of {min}",
|
||||
v = serde_json::to_string(&value).unwrap_or_default(),
|
||||
field_name = loader_field.field,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(max) = loader_field.max_val
|
||||
&& count > max
|
||||
{
|
||||
return Err(format!(
|
||||
"Provided value '{v}' for {field_name} is greater than the maximum of {max}",
|
||||
v = serde_json::to_string(&value).unwrap_or_default(),
|
||||
field_name = loader_field.field,
|
||||
));
|
||||
if let Some(max) = loader_field.max_val {
|
||||
if count > max {
|
||||
return Err(format!(
|
||||
"Provided value '{v}' for {field_name} is greater than the maximum of {max}",
|
||||
v = serde_json::to_string(&value).unwrap_or_default(),
|
||||
field_name = loader_field.field,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ pub mod team_item;
|
||||
pub mod thread_item;
|
||||
pub mod user_item;
|
||||
pub mod user_subscription_item;
|
||||
pub mod users_redeemals;
|
||||
pub mod version_item;
|
||||
|
||||
pub use collection_item::DBCollection;
|
||||
|
||||
@@ -57,6 +57,26 @@ impl DBProduct {
|
||||
Ok(Self::get_many(&[id], exec).await?.into_iter().next())
|
||||
}
|
||||
|
||||
pub async fn get_by_type<'a, E>(
|
||||
exec: E,
|
||||
r#type: &str,
|
||||
) -> Result<Vec<Self>, DatabaseError>
|
||||
where
|
||||
E: sqlx::PgExecutor<'a>,
|
||||
{
|
||||
let maybe_row = select_products_with_predicate!(
|
||||
"WHERE metadata ->> 'type' = $1",
|
||||
r#type
|
||||
)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
maybe_row
|
||||
.into_iter()
|
||||
.map(|r| r.try_into().map_err(Into::into))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn get_many(
|
||||
ids: &[DBProductId],
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
@@ -100,10 +120,11 @@ pub struct QueryProductWithPrices {
|
||||
}
|
||||
|
||||
impl QueryProductWithPrices {
|
||||
pub async fn list<'a, E>(
|
||||
/// Lists products with at least one public price.
|
||||
pub async fn list_purchaseable<'a, E>(
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<QueryProductWithPrices>, DatabaseError>
|
||||
) -> Result<Vec<Self>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
@@ -118,7 +139,51 @@ impl QueryProductWithPrices {
|
||||
}
|
||||
|
||||
let all_products = product_item::DBProduct::get_all(exec).await?;
|
||||
let prices = product_item::DBProductPrice::get_all_products_prices(
|
||||
let prices =
|
||||
product_item::DBProductPrice::get_all_public_products_prices(
|
||||
&all_products.iter().map(|x| x.id).collect::<Vec<_>>(),
|
||||
exec,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let products = all_products
|
||||
.into_iter()
|
||||
.filter_map(|x| {
|
||||
Some(QueryProductWithPrices {
|
||||
id: x.id,
|
||||
metadata: x.metadata,
|
||||
prices: prices
|
||||
.remove(&x.id)
|
||||
.map(|x| x.1)?
|
||||
.into_iter()
|
||||
.map(|x| DBProductPrice {
|
||||
id: x.id,
|
||||
product_id: x.product_id,
|
||||
prices: x.prices,
|
||||
currency_code: x.currency_code,
|
||||
})
|
||||
.collect(),
|
||||
unitary: x.unitary,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(PRODUCTS_NAMESPACE, "all", &products, None)
|
||||
.await?;
|
||||
|
||||
Ok(products)
|
||||
}
|
||||
|
||||
pub async fn list_by_product_type<'a, E>(
|
||||
exec: E,
|
||||
r#type: &str,
|
||||
) -> Result<Vec<Self>, DatabaseError>
|
||||
where
|
||||
E: sqlx::PgExecutor<'a> + Copy,
|
||||
{
|
||||
let all_products = DBProduct::get_by_type(exec, r#type).await?;
|
||||
let prices = DBProductPrice::get_all_products_prices(
|
||||
&all_products.iter().map(|x| x.id).collect::<Vec<_>>(),
|
||||
exec,
|
||||
)
|
||||
@@ -126,29 +191,26 @@ impl QueryProductWithPrices {
|
||||
|
||||
let products = all_products
|
||||
.into_iter()
|
||||
.map(|x| QueryProductWithPrices {
|
||||
id: x.id,
|
||||
metadata: x.metadata,
|
||||
prices: prices
|
||||
.remove(&x.id)
|
||||
.map(|x| x.1)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|x| DBProductPrice {
|
||||
id: x.id,
|
||||
product_id: x.product_id,
|
||||
prices: x.prices,
|
||||
currency_code: x.currency_code,
|
||||
})
|
||||
.collect(),
|
||||
unitary: x.unitary,
|
||||
.filter_map(|x| {
|
||||
Some(QueryProductWithPrices {
|
||||
id: x.id,
|
||||
metadata: x.metadata,
|
||||
prices: prices
|
||||
.remove(&x.id)
|
||||
.map(|x| x.1)?
|
||||
.into_iter()
|
||||
.map(|x| DBProductPrice {
|
||||
id: x.id,
|
||||
product_id: x.product_id,
|
||||
prices: x.prices,
|
||||
currency_code: x.currency_code,
|
||||
})
|
||||
.collect(),
|
||||
unitary: x.unitary,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(PRODUCTS_NAMESPACE, "all", &products, None)
|
||||
.await?;
|
||||
|
||||
Ok(products)
|
||||
}
|
||||
}
|
||||
@@ -169,7 +231,11 @@ struct ProductPriceQueryResult {
|
||||
}
|
||||
|
||||
macro_rules! select_prices_with_predicate {
|
||||
($predicate:tt, $param:ident) => {
|
||||
($predicate:tt, $param1:ident) => {
|
||||
select_prices_with_predicate!($predicate, $param1, )
|
||||
};
|
||||
|
||||
($predicate:tt, $($param:ident,)+) => {
|
||||
sqlx::query_as!(
|
||||
ProductPriceQueryResult,
|
||||
r#"
|
||||
@@ -177,7 +243,7 @@ macro_rules! select_prices_with_predicate {
|
||||
FROM products_prices
|
||||
"#
|
||||
+ $predicate,
|
||||
$param
|
||||
$($param),+
|
||||
)
|
||||
};
|
||||
}
|
||||
@@ -231,33 +297,82 @@ impl DBProductPrice {
|
||||
Ok(res.remove(&product_id).map(|x| x.1).unwrap_or_default())
|
||||
}
|
||||
|
||||
pub async fn get_all_public_product_prices(
|
||||
product_id: DBProductId,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Vec<DBProductPrice>, DatabaseError> {
|
||||
let res =
|
||||
Self::get_all_public_products_prices(&[product_id], exec).await?;
|
||||
|
||||
Ok(res.remove(&product_id).map(|x| x.1).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Gets all public prices for the given products. If a product has no public price,
|
||||
/// it won't be included in the resulting map.
|
||||
pub async fn get_all_public_products_prices(
|
||||
product_ids: &[DBProductId],
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<DashMap<DBProductId, Vec<DBProductPrice>>, DatabaseError> {
|
||||
Self::get_all_products_prices_with_visibility(
|
||||
product_ids,
|
||||
Some(true),
|
||||
exec,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_all_products_prices(
|
||||
product_ids: &[DBProductId],
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<DashMap<DBProductId, Vec<DBProductPrice>>, DatabaseError> {
|
||||
Self::get_all_products_prices_with_visibility(product_ids, None, exec)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_products_prices_with_visibility(
|
||||
product_ids: &[DBProductId],
|
||||
public_filter: Option<bool>,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<DashMap<DBProductId, Vec<DBProductPrice>>, DatabaseError> {
|
||||
let ids = product_ids.iter().map(|id| id.0).collect_vec();
|
||||
let ids_ref: &[i64] = &ids;
|
||||
|
||||
use futures_util::TryStreamExt;
|
||||
let prices = select_prices_with_predicate!(
|
||||
"WHERE product_id = ANY($1::bigint[])",
|
||||
ids_ref
|
||||
)
|
||||
.fetch(exec)
|
||||
.try_fold(
|
||||
DashMap::new(),
|
||||
|acc: DashMap<DBProductId, Vec<DBProductPrice>>, x| {
|
||||
if let Ok(item) = <ProductPriceQueryResult as TryInto<
|
||||
DBProductPrice,
|
||||
>>::try_into(x)
|
||||
{
|
||||
acc.entry(item.product_id).or_default().push(item);
|
||||
}
|
||||
|
||||
async move { Ok(acc) }
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let predicate = |acc: DashMap<DBProductId, Vec<DBProductPrice>>, x| {
|
||||
if let Ok(item) = <ProductPriceQueryResult as TryInto<
|
||||
DBProductPrice,
|
||||
>>::try_into(x)
|
||||
{
|
||||
acc.entry(item.product_id).or_default().push(item);
|
||||
}
|
||||
|
||||
async move { Ok(acc) }
|
||||
};
|
||||
|
||||
let prices = match public_filter {
|
||||
None => {
|
||||
select_prices_with_predicate!(
|
||||
"WHERE product_id = ANY($1::bigint[])",
|
||||
ids_ref,
|
||||
)
|
||||
.fetch(exec)
|
||||
.try_fold(DashMap::new(), predicate)
|
||||
.await?
|
||||
}
|
||||
|
||||
Some(public) => {
|
||||
select_prices_with_predicate!(
|
||||
"WHERE product_id = ANY($1::bigint[])
|
||||
AND public = $2",
|
||||
ids_ref,
|
||||
public,
|
||||
)
|
||||
.fetch(exec)
|
||||
.try_fold(DashMap::new(), predicate)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(prices)
|
||||
}
|
||||
|
||||
@@ -483,20 +483,20 @@ impl DBTeamMember {
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(accepted) = new_accepted
|
||||
&& accepted
|
||||
{
|
||||
sqlx::query!(
|
||||
"
|
||||
if let Some(accepted) = new_accepted {
|
||||
if accepted {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE team_members
|
||||
SET accepted = TRUE
|
||||
WHERE (team_id = $1 AND user_id = $2)
|
||||
",
|
||||
id as DBTeamId,
|
||||
user_id as DBUserId,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
id as DBTeamId,
|
||||
user_id as DBUserId,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(payouts_split) = new_payouts_split {
|
||||
|
||||
301
apps/labrinth/src/database/models/users_redeemals.rs
Normal file
301
apps/labrinth/src/database/models/users_redeemals.rs
Normal file
@@ -0,0 +1,301 @@
|
||||
use crate::database::models::DBUserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{query, query_scalar};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Offer {
|
||||
#[default]
|
||||
Medal,
|
||||
}
|
||||
|
||||
impl Offer {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Offer::Medal => "medal",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str_or_default(s: &str) -> Self {
|
||||
match s {
|
||||
"medal" => Offer::Medal,
|
||||
_ => Offer::Medal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Offer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Status {
|
||||
#[default]
|
||||
Pending,
|
||||
Processing,
|
||||
Processed,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Status::Pending => "pending",
|
||||
Status::Processing => "processing",
|
||||
Status::Processed => "processed",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str_or_default(s: &str) -> Self {
|
||||
match s {
|
||||
"pending" => Status::Pending,
|
||||
"processing" => Status::Processing,
|
||||
"processed" => Status::Processed,
|
||||
_ => Status::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Status {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserRedeemal {
|
||||
pub id: i32,
|
||||
pub user_id: DBUserId,
|
||||
pub offer: Offer,
|
||||
pub redeemed: DateTime<Utc>,
|
||||
pub last_attempt: Option<DateTime<Utc>>,
|
||||
pub n_attempts: i32,
|
||||
pub status: Status,
|
||||
}
|
||||
|
||||
impl UserRedeemal {
|
||||
pub async fn get_pending<'a, E>(
|
||||
exec: E,
|
||||
limit: i64,
|
||||
) -> sqlx::Result<Vec<UserRedeemal>>
|
||||
where
|
||||
E: sqlx::PgExecutor<'a>,
|
||||
{
|
||||
let redeemals = query!(
|
||||
r#"SELECT * FROM users_redeemals WHERE status = $1 LIMIT $2"#,
|
||||
Status::Pending.as_str(),
|
||||
limit
|
||||
)
|
||||
.fetch_all(exec)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|row| UserRedeemal {
|
||||
id: row.id,
|
||||
user_id: DBUserId(row.user_id),
|
||||
offer: Offer::from_str_or_default(&row.offer),
|
||||
redeemed: row.redeemed,
|
||||
last_attempt: row.last_attempt,
|
||||
n_attempts: row.n_attempts,
|
||||
status: Status::from_str_or_default(&row.status),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(redeemals)
|
||||
}
|
||||
|
||||
pub async fn update_stuck_5_minutes<'a, E>(exec: E) -> sqlx::Result<()>
|
||||
where
|
||||
E: sqlx::PgExecutor<'a>,
|
||||
{
|
||||
query!(
|
||||
r#"
|
||||
UPDATE users_redeemals
|
||||
SET status = $1
|
||||
WHERE
|
||||
status = $2
|
||||
AND NOW() - last_attempt > INTERVAL '5 minutes'
|
||||
"#,
|
||||
Status::Pending.as_str(),
|
||||
Status::Processing.as_str(),
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn exists_by_user_and_offer<'a, E>(
|
||||
exec: E,
|
||||
user_id: DBUserId,
|
||||
offer: Offer,
|
||||
) -> sqlx::Result<bool>
|
||||
where
|
||||
E: sqlx::PgExecutor<'a>,
|
||||
{
|
||||
query_scalar!(
|
||||
r#"SELECT
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
users_redeemals
|
||||
WHERE
|
||||
user_id = $1
|
||||
AND offer = $2
|
||||
) AS "exists!"
|
||||
"#,
|
||||
user_id.0,
|
||||
offer.as_str(),
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn insert<'a, E>(&mut self, exec: E) -> sqlx::Result<()>
|
||||
where
|
||||
E: sqlx::PgExecutor<'a>,
|
||||
{
|
||||
let query = query_scalar!(
|
||||
r#"
|
||||
INSERT INTO users_redeemals
|
||||
(user_id, offer, redeemed, status, last_attempt, n_attempts)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id"#,
|
||||
self.user_id.0,
|
||||
self.offer.as_str(),
|
||||
self.redeemed,
|
||||
self.status.as_str(),
|
||||
self.last_attempt,
|
||||
self.n_attempts,
|
||||
);
|
||||
|
||||
let id = query.fetch_one(exec).await?;
|
||||
|
||||
self.id = id;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates `status`, `last_attempt`, and `n_attempts` only if `status` is currently pending.
|
||||
/// Returns `true` if the status was updated, `false` otherwise.
|
||||
pub async fn update_status_if_pending<'a, E>(
|
||||
&self,
|
||||
exec: E,
|
||||
) -> sqlx::Result<bool>
|
||||
where
|
||||
E: sqlx::PgExecutor<'a>,
|
||||
{
|
||||
let query = query!(
|
||||
r#"
|
||||
UPDATE users_redeemals
|
||||
SET
|
||||
status = $3,
|
||||
last_attempt = $4,
|
||||
n_attempts = $5
|
||||
WHERE id = $1 AND status = $2
|
||||
"#,
|
||||
self.id,
|
||||
Status::Pending.as_str(),
|
||||
self.status.as_str(),
|
||||
self.last_attempt,
|
||||
self.n_attempts,
|
||||
);
|
||||
|
||||
let query_result = query.execute(exec).await?;
|
||||
|
||||
Ok(query_result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
pub async fn update<'a, E>(&self, exec: E) -> sqlx::Result<()>
|
||||
where
|
||||
E: sqlx::PgExecutor<'a>,
|
||||
{
|
||||
let query = query!(
|
||||
r#"
|
||||
UPDATE users_redeemals
|
||||
SET
|
||||
offer = $2,
|
||||
status = $3,
|
||||
redeemed = $4,
|
||||
last_attempt = $5,
|
||||
n_attempts = $6
|
||||
WHERE id = $1
|
||||
"#,
|
||||
self.id,
|
||||
self.offer.as_str(),
|
||||
self.status.as_str(),
|
||||
self.redeemed,
|
||||
self.last_attempt,
|
||||
self.n_attempts,
|
||||
);
|
||||
|
||||
query.execute(exec).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RedeemalLookupFields {
|
||||
pub user_id: DBUserId,
|
||||
pub redeemal_status: Option<Status>,
|
||||
}
|
||||
|
||||
impl RedeemalLookupFields {
|
||||
/// Returns the redeemal status of a user for an offer, while looking up the user
|
||||
/// itself. **This expects a single redeemal per user/offer pair**.
|
||||
///
|
||||
/// If the returned value is `Ok(None)`, the user doesn't exist.
|
||||
///
|
||||
/// If the returned value is `Ok(Some(fields))`, but `redeemal_status` is `None`,
|
||||
/// the user exists and has not redeemed the offer.
|
||||
pub async fn redeemal_status_by_username_and_offer<'a, E>(
|
||||
exec: E,
|
||||
user_username: &str,
|
||||
offer: Offer,
|
||||
) -> sqlx::Result<Option<RedeemalLookupFields>>
|
||||
where
|
||||
E: sqlx::PgExecutor<'a>,
|
||||
{
|
||||
let maybe_row = query!(
|
||||
r#"
|
||||
SELECT
|
||||
users.id,
|
||||
users_redeemals.status AS "status: Option<String>"
|
||||
FROM
|
||||
users
|
||||
LEFT JOIN
|
||||
users_redeemals ON users_redeemals.user_id = users.id
|
||||
AND users_redeemals.offer = $2
|
||||
WHERE
|
||||
users.username = $1
|
||||
ORDER BY
|
||||
users_redeemals.redeemed DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
user_username,
|
||||
offer.as_str(),
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
// If no row was returned, the user doesn't exist.
|
||||
// If a row NULL status was returned, the user exists but has no redeemed the offer.
|
||||
|
||||
Ok(maybe_row.map(|row| RedeemalLookupFields {
|
||||
user_id: DBUserId(row.id),
|
||||
redeemal_status: row
|
||||
.status
|
||||
.as_deref()
|
||||
.map(Status::from_str_or_default),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -353,10 +353,10 @@ impl RedisPool {
|
||||
};
|
||||
|
||||
for (idx, key) in fetch_ids.into_iter().enumerate() {
|
||||
if let Some(locked) = results.get(idx)
|
||||
&& locked.is_none()
|
||||
{
|
||||
continue;
|
||||
if let Some(locked) = results.get(idx) {
|
||||
if locked.is_none() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((key, raw_key)) = ids.remove(&key) {
|
||||
|
||||
@@ -334,14 +334,18 @@ impl From<Version> for LegacyVersion {
|
||||
// the v2 loaders are whatever the corresponding loader fields are
|
||||
let mut loaders =
|
||||
data.loaders.into_iter().map(|l| l.0).collect::<Vec<_>>();
|
||||
if loaders.contains(&"mrpack".to_string())
|
||||
&& let Some((_, mrpack_loaders)) = data
|
||||
if loaders.contains(&"mrpack".to_string()) {
|
||||
if let Some((_, mrpack_loaders)) = data
|
||||
.fields
|
||||
.into_iter()
|
||||
.find(|(key, _)| key == "mrpack_loaders")
|
||||
&& let Ok(mrpack_loaders) = serde_json::from_value(mrpack_loaders)
|
||||
{
|
||||
loaders = mrpack_loaders;
|
||||
{
|
||||
if let Ok(mrpack_loaders) =
|
||||
serde_json::from_value(mrpack_loaders)
|
||||
{
|
||||
loaders = mrpack_loaders;
|
||||
}
|
||||
}
|
||||
}
|
||||
let loaders = loaders.into_iter().map(Loader).collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -43,33 +43,35 @@ impl LegacyResultSearchProject {
|
||||
pub fn from(result_search_project: ResultSearchProject) -> Self {
|
||||
let mut categories = result_search_project.categories;
|
||||
categories.extend(result_search_project.loaders.clone());
|
||||
if categories.contains(&"mrpack".to_string())
|
||||
&& let Some(mrpack_loaders) = result_search_project
|
||||
if categories.contains(&"mrpack".to_string()) {
|
||||
if let Some(mrpack_loaders) = result_search_project
|
||||
.project_loader_fields
|
||||
.get("mrpack_loaders")
|
||||
{
|
||||
categories.extend(
|
||||
mrpack_loaders
|
||||
.iter()
|
||||
.filter_map(|c| c.as_str())
|
||||
.map(String::from),
|
||||
);
|
||||
categories.retain(|c| c != "mrpack");
|
||||
{
|
||||
categories.extend(
|
||||
mrpack_loaders
|
||||
.iter()
|
||||
.filter_map(|c| c.as_str())
|
||||
.map(String::from),
|
||||
);
|
||||
categories.retain(|c| c != "mrpack");
|
||||
}
|
||||
}
|
||||
let mut display_categories = result_search_project.display_categories;
|
||||
display_categories.extend(result_search_project.loaders);
|
||||
if display_categories.contains(&"mrpack".to_string())
|
||||
&& let Some(mrpack_loaders) = result_search_project
|
||||
if display_categories.contains(&"mrpack".to_string()) {
|
||||
if let Some(mrpack_loaders) = result_search_project
|
||||
.project_loader_fields
|
||||
.get("mrpack_loaders")
|
||||
{
|
||||
categories.extend(
|
||||
mrpack_loaders
|
||||
.iter()
|
||||
.filter_map(|c| c.as_str())
|
||||
.map(String::from),
|
||||
);
|
||||
display_categories.retain(|c| c != "mrpack");
|
||||
{
|
||||
categories.extend(
|
||||
mrpack_loaders
|
||||
.iter()
|
||||
.filter_map(|c| c.as_str())
|
||||
.map(String::from),
|
||||
);
|
||||
display_categories.retain(|c| c != "mrpack");
|
||||
}
|
||||
}
|
||||
|
||||
// Sort then remove duplicates
|
||||
|
||||
@@ -24,6 +24,13 @@ pub enum ProductMetadata {
|
||||
swap: u32,
|
||||
storage: u32,
|
||||
},
|
||||
Medal {
|
||||
cpu: u32,
|
||||
ram: u32,
|
||||
swap: u32,
|
||||
storage: u32,
|
||||
region: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -48,6 +55,7 @@ pub enum Price {
|
||||
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Debug, Copy, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PriceDuration {
|
||||
FiveDays,
|
||||
Monthly,
|
||||
Quarterly,
|
||||
Yearly,
|
||||
@@ -56,6 +64,7 @@ pub enum PriceDuration {
|
||||
impl PriceDuration {
|
||||
pub fn duration(&self) -> chrono::Duration {
|
||||
match self {
|
||||
PriceDuration::FiveDays => chrono::Duration::days(5),
|
||||
PriceDuration::Monthly => chrono::Duration::days(30),
|
||||
PriceDuration::Quarterly => chrono::Duration::days(90),
|
||||
PriceDuration::Yearly => chrono::Duration::days(365),
|
||||
@@ -64,6 +73,7 @@ impl PriceDuration {
|
||||
|
||||
pub fn from_string(string: &str) -> PriceDuration {
|
||||
match string {
|
||||
"five-days" => PriceDuration::FiveDays,
|
||||
"monthly" => PriceDuration::Monthly,
|
||||
"quarterly" => PriceDuration::Quarterly,
|
||||
"yearly" => PriceDuration::Yearly,
|
||||
@@ -76,6 +86,7 @@ impl PriceDuration {
|
||||
PriceDuration::Monthly => "monthly",
|
||||
PriceDuration::Quarterly => "quarterly",
|
||||
PriceDuration::Yearly => "yearly",
|
||||
PriceDuration::FiveDays => "five-days",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +95,7 @@ impl PriceDuration {
|
||||
PriceDuration::Monthly,
|
||||
PriceDuration::Quarterly,
|
||||
PriceDuration::Yearly,
|
||||
PriceDuration::FiveDays,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
@@ -146,6 +158,7 @@ impl SubscriptionStatus {
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum SubscriptionMetadata {
|
||||
Pyro { id: String, region: Option<String> },
|
||||
Medal { id: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -207,6 +220,10 @@ pub enum ChargeStatus {
|
||||
Succeeded,
|
||||
Failed,
|
||||
Cancelled,
|
||||
// Expiring charges are charges that aren't expected to be processed
|
||||
// but can be promoted to a full charge, like for trials/freebies. When
|
||||
// due, the underlying subscription is unprovisioned.
|
||||
Expiring,
|
||||
}
|
||||
|
||||
impl ChargeStatus {
|
||||
@@ -217,6 +234,7 @@ impl ChargeStatus {
|
||||
"failed" => ChargeStatus::Failed,
|
||||
"open" => ChargeStatus::Open,
|
||||
"cancelled" => ChargeStatus::Cancelled,
|
||||
"expiring" => ChargeStatus::Expiring,
|
||||
_ => ChargeStatus::Failed,
|
||||
}
|
||||
}
|
||||
@@ -228,6 +246,7 @@ impl ChargeStatus {
|
||||
ChargeStatus::Failed => "failed",
|
||||
ChargeStatus::Open => "open",
|
||||
ChargeStatus::Cancelled => "cancelled",
|
||||
ChargeStatus::Expiring => "expiring",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,12 +254,14 @@ impl ChargeStatus {
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PaymentPlatform {
|
||||
Stripe,
|
||||
None,
|
||||
}
|
||||
|
||||
impl PaymentPlatform {
|
||||
pub fn from_string(string: &str) -> PaymentPlatform {
|
||||
match string {
|
||||
"stripe" => PaymentPlatform::Stripe,
|
||||
"none" => PaymentPlatform::None,
|
||||
_ => PaymentPlatform::Stripe,
|
||||
}
|
||||
}
|
||||
@@ -248,6 +269,7 @@ impl PaymentPlatform {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
PaymentPlatform::Stripe => "stripe",
|
||||
PaymentPlatform::None => "none",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,10 +166,10 @@ impl From<ProjectQueryResult> for Project {
|
||||
Ok(spdx_expr) => {
|
||||
let mut vec: Vec<&str> = Vec::new();
|
||||
for node in spdx_expr.iter() {
|
||||
if let spdx::expression::ExprNode::Req(req) = node
|
||||
&& let Some(id) = req.req.license.id()
|
||||
{
|
||||
vec.push(id.full_name);
|
||||
if let spdx::expression::ExprNode::Req(req) = node {
|
||||
if let Some(id) = req.req.license.id() {
|
||||
vec.push(id.full_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
// spdx crate returns AND/OR operations in postfix order
|
||||
|
||||
@@ -51,16 +51,16 @@ impl ProjectPermissions {
|
||||
return Some(ProjectPermissions::all());
|
||||
}
|
||||
|
||||
if let Some(member) = project_team_member
|
||||
&& member.accepted
|
||||
{
|
||||
return Some(member.permissions);
|
||||
if let Some(member) = project_team_member {
|
||||
if member.accepted {
|
||||
return Some(member.permissions);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(member) = organization_team_member
|
||||
&& member.accepted
|
||||
{
|
||||
return Some(member.permissions);
|
||||
if let Some(member) = organization_team_member {
|
||||
if member.accepted {
|
||||
return Some(member.permissions);
|
||||
}
|
||||
}
|
||||
|
||||
if role.is_mod() {
|
||||
@@ -107,10 +107,10 @@ impl OrganizationPermissions {
|
||||
return Some(OrganizationPermissions::all());
|
||||
}
|
||||
|
||||
if let Some(member) = team_member
|
||||
&& member.accepted
|
||||
{
|
||||
return member.organization_permissions;
|
||||
if let Some(member) = team_member {
|
||||
if member.accepted {
|
||||
return member.organization_permissions;
|
||||
}
|
||||
}
|
||||
if role.is_mod() {
|
||||
return Some(
|
||||
|
||||
@@ -45,15 +45,17 @@ impl MaxMindIndexer {
|
||||
|
||||
if let Ok(entries) = archive.entries() {
|
||||
for mut file in entries.flatten() {
|
||||
if let Ok(path) = file.header().path()
|
||||
&& path.extension().and_then(|x| x.to_str()) == Some("mmdb")
|
||||
{
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
if let Ok(path) = file.header().path() {
|
||||
if path.extension().and_then(|x| x.to_str()) == Some("mmdb")
|
||||
{
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
|
||||
let reader = maxminddb::Reader::from_source(buf).unwrap();
|
||||
let reader =
|
||||
maxminddb::Reader::from_source(buf).unwrap();
|
||||
|
||||
return Ok(Some(reader));
|
||||
return Ok(Some(reader));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,8 +371,8 @@ impl AutomatedModerationQueue {
|
||||
for file in
|
||||
files.iter().filter(|x| x.version_id == version.id.into())
|
||||
{
|
||||
if let Some(hash) = file.hashes.get("sha1")
|
||||
&& let Some((index, (sha1, _, file_name, _))) = hashes
|
||||
if let Some(hash) = file.hashes.get("sha1") {
|
||||
if let Some((index, (sha1, _, file_name, _))) = hashes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, (value, _, _, _))| value == hash)
|
||||
@@ -382,6 +382,7 @@ impl AutomatedModerationQueue {
|
||||
|
||||
hashes.remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,11 +420,12 @@ impl AutomatedModerationQueue {
|
||||
.await?;
|
||||
|
||||
for row in rows {
|
||||
if let Some(sha1) = row.sha1
|
||||
&& let Some((index, (sha1, _, file_name, _))) = hashes.iter().enumerate().find(|(_, (value, _, _, _))| value == &sha1) {
|
||||
if let Some(sha1) = row.sha1 {
|
||||
if 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) });
|
||||
hashes.remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hashes.is_empty() {
|
||||
@@ -497,8 +499,8 @@ impl AutomatedModerationQueue {
|
||||
let mut insert_ids = Vec::new();
|
||||
|
||||
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)
|
||||
&& let Some((index, (sha1, _, file_name, _))) = hashes.iter().enumerate().find(|(_, (value, _, _, _))| value == hash) {
|
||||
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) {
|
||||
final_hashes.insert(sha1.clone(), IdentifiedFile {
|
||||
file_name: file_name.clone(),
|
||||
status: ApprovalType::from_string(&row.status).unwrap_or(ApprovalType::Unidentified),
|
||||
@@ -510,6 +512,7 @@ impl AutomatedModerationQueue {
|
||||
hashes.remove(index);
|
||||
flame_files.remove(curse_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !insert_ids.is_empty() && !insert_hashes.is_empty() {
|
||||
@@ -578,8 +581,8 @@ impl AutomatedModerationQueue {
|
||||
for (sha1, _pack_file, file_name, _mumur2) in hashes {
|
||||
let flame_file = flame_files.iter().find(|x| x.0 == sha1);
|
||||
|
||||
if let Some((_, flame_project_id)) = flame_file
|
||||
&& let Some(project) = flame_projects.iter().find(|x| &x.id == flame_project_id) {
|
||||
if let Some((_, flame_project_id)) = flame_file {
|
||||
if let Some(project) = flame_projects.iter().find(|x| &x.id == flame_project_id) {
|
||||
missing_metadata.flame_files.insert(sha1, MissingMetadataFlame {
|
||||
title: project.name.clone(),
|
||||
file_name,
|
||||
@@ -589,6 +592,7 @@ impl AutomatedModerationQueue {
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
missing_metadata.unknown_files.insert(sha1, file_name);
|
||||
}
|
||||
|
||||
@@ -257,30 +257,31 @@ impl PayoutsQueue {
|
||||
)
|
||||
})?;
|
||||
|
||||
if !status.is_success()
|
||||
&& let Some(obj) = value.as_object()
|
||||
{
|
||||
if let Some(array) = obj.get("errors") {
|
||||
#[derive(Deserialize)]
|
||||
struct TremendousError {
|
||||
message: String,
|
||||
if !status.is_success() {
|
||||
if let Some(obj) = value.as_object() {
|
||||
if let Some(array) = obj.get("errors") {
|
||||
#[derive(Deserialize)]
|
||||
struct TremendousError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
let err = serde_json::from_value::<TremendousError>(
|
||||
array.clone(),
|
||||
)
|
||||
.map_err(|_| {
|
||||
ApiError::Payments(
|
||||
"could not retrieve Tremendous error json body"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
return Err(ApiError::Payments(err.message));
|
||||
}
|
||||
|
||||
let err =
|
||||
serde_json::from_value::<TremendousError>(array.clone())
|
||||
.map_err(|_| {
|
||||
ApiError::Payments(
|
||||
"could not retrieve Tremendous error json body"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
return Err(ApiError::Payments(err.message));
|
||||
return Err(ApiError::Payments(
|
||||
"could not retrieve Tremendous error body".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
return Err(ApiError::Payments(
|
||||
"could not retrieve Tremendous error body".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(serde_json::from_value(value)?)
|
||||
@@ -448,10 +449,10 @@ impl PayoutsQueue {
|
||||
};
|
||||
|
||||
// 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
|
||||
&& !product.currency_codes.contains(&"USD".to_string())
|
||||
{
|
||||
continue;
|
||||
if let PayoutInterval::Fixed { .. } = method.interval {
|
||||
if !product.currency_codes.contains(&"USD".to_string()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
methods.push(method);
|
||||
|
||||
0
apps/labrinth/src/queue/servers.rs
Normal file
0
apps/labrinth/src/queue/servers.rs
Normal file
@@ -1,5 +1,8 @@
|
||||
use crate::auth::{get_user_from_headers, send_email};
|
||||
use crate::database::models::charge_item::DBCharge;
|
||||
use crate::database::models::user_item::DBUser;
|
||||
use crate::database::models::user_subscription_item::DBUserSubscription;
|
||||
use crate::database::models::users_redeemals::{self, UserRedeemal};
|
||||
use crate::database::models::{
|
||||
generate_charge_id, generate_user_subscription_id, product_item,
|
||||
user_subscription_item,
|
||||
@@ -14,6 +17,7 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::users::Badges;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::archon::{ArchonClient, CreateServerRequest, Specs};
|
||||
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
|
||||
use ariadne::ids::base62_impl::{parse_base62, to_base62};
|
||||
use chrono::{Duration, Utc};
|
||||
@@ -59,8 +63,10 @@ pub async fn products(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let products =
|
||||
product_item::QueryProductWithPrices::list(&**pool, &redis).await?;
|
||||
let products = product_item::QueryProductWithPrices::list_purchaseable(
|
||||
&**pool, &redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let products = products
|
||||
.into_iter()
|
||||
@@ -182,7 +188,9 @@ pub async fn refund_charge(
|
||||
ChargeStatus::Open
|
||||
| ChargeStatus::Processing
|
||||
| ChargeStatus::Succeeded => Some(x.amount),
|
||||
ChargeStatus::Failed | ChargeStatus::Cancelled => None,
|
||||
ChargeStatus::Failed
|
||||
| ChargeStatus::Cancelled
|
||||
| ChargeStatus::Expiring => None,
|
||||
})
|
||||
.sum::<i64>();
|
||||
|
||||
@@ -256,6 +264,12 @@ pub async fn refund_charge(
|
||||
));
|
||||
}
|
||||
}
|
||||
PaymentPlatform::None => {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"This charge was not processed via a payment platform."
|
||||
.to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -286,17 +300,17 @@ pub async fn refund_charge(
|
||||
.upsert(&mut transaction)
|
||||
.await?;
|
||||
|
||||
if body.0.unprovision.unwrap_or(false)
|
||||
&& let Some(subscription_id) = charge.subscription_id
|
||||
{
|
||||
let open_charge =
|
||||
DBCharge::get_open_subscription(subscription_id, &**pool)
|
||||
.await?;
|
||||
if let Some(mut open_charge) = open_charge {
|
||||
open_charge.status = ChargeStatus::Cancelled;
|
||||
open_charge.due = Utc::now();
|
||||
if body.0.unprovision.unwrap_or(false) {
|
||||
if let Some(subscription_id) = charge.subscription_id {
|
||||
let open_charge =
|
||||
DBCharge::get_open_subscription(subscription_id, &**pool)
|
||||
.await?;
|
||||
if let Some(mut open_charge) = open_charge {
|
||||
open_charge.status = ChargeStatus::Cancelled;
|
||||
open_charge.due = Utc::now();
|
||||
|
||||
open_charge.upsert(&mut transaction).await?;
|
||||
open_charge.upsert(&mut transaction).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,6 +384,7 @@ pub async fn edit_subscription(
|
||||
})?;
|
||||
|
||||
if let Some(cancelled) = &edit_subscription.cancelled {
|
||||
// Notably, cannot cancel/uncancel expiring charges.
|
||||
if !matches!(
|
||||
open_charge.status,
|
||||
ChargeStatus::Open
|
||||
@@ -392,16 +407,20 @@ pub async fn edit_subscription(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(interval) = &edit_subscription.interval
|
||||
&& let Price::Recurring { intervals } = ¤t_price.prices
|
||||
{
|
||||
if let Some(price) = intervals.get(interval) {
|
||||
open_charge.subscription_interval = Some(*interval);
|
||||
open_charge.amount = *price as i64;
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Interval is not valid for this subscription!".to_string(),
|
||||
));
|
||||
if let Some(interval) = &edit_subscription.interval {
|
||||
if let Price::Recurring { intervals } = ¤t_price.prices {
|
||||
// For expiring charges, the interval is handled in the Product branch.
|
||||
if open_charge.status != ChargeStatus::Expiring {
|
||||
if let Some(price) = intervals.get(interval) {
|
||||
open_charge.subscription_interval = Some(*interval);
|
||||
open_charge.amount = *price as i64;
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Interval is not valid for this subscription!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,48 +447,14 @@ pub async fn edit_subscription(
|
||||
));
|
||||
}
|
||||
|
||||
let interval = open_charge.due - Utc::now();
|
||||
let duration = PriceDuration::Monthly;
|
||||
// If the charge is an expiring charge, we need to create a payment
|
||||
// intent as if the user was subscribing to the product, as opposed
|
||||
// to a proration.
|
||||
if open_charge.status == ChargeStatus::Expiring {
|
||||
// We need a new interval when promoting the charge.
|
||||
let interval = edit_subscription.interval
|
||||
.ok_or_else(|| ApiError::InvalidInput("You need to specify an interval when promoting an expiring charge.".to_owned()))?;
|
||||
|
||||
let current_amount = match ¤t_price.prices {
|
||||
Price::OneTime { price } => *price,
|
||||
Price::Recurring { intervals } => *intervals.get(&duration).ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"Could not find a valid price for the user's duration".to_string(),
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
let amount = match &product_price.prices {
|
||||
Price::OneTime { price } => *price,
|
||||
Price::Recurring { intervals } => *intervals.get(&duration).ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"Could not find a valid price for the user's duration".to_string(),
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
let complete = Decimal::from(interval.num_seconds())
|
||||
/ Decimal::from(duration.duration().num_seconds());
|
||||
let proration = (Decimal::from(amount - current_amount) * complete)
|
||||
.floor()
|
||||
.to_i32()
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"Could not convert proration to i32".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// First branch: Plan downgrade, update future charge
|
||||
// Second branch: For small transactions (under 30 cents), we make a loss on the
|
||||
// proration due to fees. In these situations, just give it to them for free, because
|
||||
// their next charge will be in a day or two anyway.
|
||||
if current_amount > amount || proration < 30 {
|
||||
open_charge.price_id = product_price.id;
|
||||
open_charge.amount = amount as i64;
|
||||
|
||||
None
|
||||
} else {
|
||||
let charge_id = generate_charge_id(&mut transaction).await?;
|
||||
|
||||
let customer_id = get_or_create_customer(
|
||||
@@ -482,6 +467,15 @@ pub async fn edit_subscription(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let new_price_value = match product_price.prices {
|
||||
Price::OneTime { ref price } => *price,
|
||||
Price::Recurring { ref intervals } => {
|
||||
*intervals
|
||||
.get(&interval)
|
||||
.ok_or_else(|| ApiError::InvalidInput("Could not find a valid price for the specified duration".to_owned()))?
|
||||
}
|
||||
};
|
||||
|
||||
let currency = Currency::from_str(
|
||||
¤t_price.currency_code.to_lowercase(),
|
||||
)
|
||||
@@ -490,7 +484,7 @@ pub async fn edit_subscription(
|
||||
})?;
|
||||
|
||||
let mut intent =
|
||||
CreatePaymentIntent::new(proration as i64, currency);
|
||||
CreatePaymentIntent::new(new_price_value as i64, currency);
|
||||
|
||||
let mut metadata = HashMap::new();
|
||||
metadata.insert(
|
||||
@@ -511,15 +505,11 @@ pub async fn edit_subscription(
|
||||
);
|
||||
metadata.insert(
|
||||
"modrinth_subscription_interval".to_string(),
|
||||
open_charge
|
||||
.subscription_interval
|
||||
.unwrap_or(PriceDuration::Monthly)
|
||||
.as_str()
|
||||
.to_string(),
|
||||
interval.as_str().to_string(),
|
||||
);
|
||||
metadata.insert(
|
||||
"modrinth_charge_type".to_string(),
|
||||
ChargeType::Proration.as_str().to_string(),
|
||||
ChargeType::Subscription.as_str().to_string(),
|
||||
);
|
||||
|
||||
intent.customer = Some(customer_id);
|
||||
@@ -544,7 +534,139 @@ pub async fn edit_subscription(
|
||||
stripe::PaymentIntent::create(&stripe_client, intent)
|
||||
.await?;
|
||||
|
||||
Some((proration, 0, intent))
|
||||
// We do NOT update the open charge here. It will be patched to be the next
|
||||
// charge of the subscription in the stripe webhook.
|
||||
//
|
||||
// We also shouldn't delete it, because if the payment fails, the expiring
|
||||
// charge will be gone and the preview subscription will never be unprovisioned.
|
||||
|
||||
Some((new_price_value, 0, intent))
|
||||
} else {
|
||||
// The charge is not an expiring charge, need to prorate.
|
||||
|
||||
let interval = open_charge.due - Utc::now();
|
||||
let duration = PriceDuration::Monthly;
|
||||
|
||||
let current_amount = match ¤t_price.prices {
|
||||
Price::OneTime { price } => *price,
|
||||
Price::Recurring { intervals } => *intervals.get(&duration).ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"Could not find a valid price for the user's duration".to_string(),
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
let amount = match &product_price.prices {
|
||||
Price::OneTime { price } => *price,
|
||||
Price::Recurring { intervals } => *intervals.get(&duration).ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"Could not find a valid price for the user's duration".to_string(),
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
let complete = Decimal::from(interval.num_seconds())
|
||||
/ Decimal::from(duration.duration().num_seconds());
|
||||
let proration = (Decimal::from(amount - current_amount)
|
||||
* complete)
|
||||
.floor()
|
||||
.to_i32()
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"Could not convert proration to i32".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// First condition: Plan downgrade, update future charge
|
||||
// Second condition: For small transactions (under 30 cents), we make a loss on the
|
||||
// proration due to fees. In these situations, just give it to them for free, because
|
||||
// their next charge will be in a day or two anyway.
|
||||
if current_amount > amount || proration < 30 {
|
||||
open_charge.price_id = product_price.id;
|
||||
open_charge.amount = amount as i64;
|
||||
|
||||
None
|
||||
} else {
|
||||
let charge_id =
|
||||
generate_charge_id(&mut transaction).await?;
|
||||
|
||||
let customer_id = get_or_create_customer(
|
||||
user.id,
|
||||
user.stripe_customer_id.as_deref(),
|
||||
user.email.as_deref(),
|
||||
&stripe_client,
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let currency = Currency::from_str(
|
||||
¤t_price.currency_code.to_lowercase(),
|
||||
)
|
||||
.map_err(|_| {
|
||||
ApiError::InvalidInput(
|
||||
"Invalid currency code".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut intent =
|
||||
CreatePaymentIntent::new(proration as i64, currency);
|
||||
|
||||
let mut metadata = HashMap::new();
|
||||
metadata.insert(
|
||||
"modrinth_user_id".to_string(),
|
||||
to_base62(user.id.0),
|
||||
);
|
||||
metadata.insert(
|
||||
"modrinth_charge_id".to_string(),
|
||||
to_base62(charge_id.0 as u64),
|
||||
);
|
||||
metadata.insert(
|
||||
"modrinth_subscription_id".to_string(),
|
||||
to_base62(subscription.id.0 as u64),
|
||||
);
|
||||
metadata.insert(
|
||||
"modrinth_price_id".to_string(),
|
||||
to_base62(product_price.id.0 as u64),
|
||||
);
|
||||
metadata.insert(
|
||||
"modrinth_subscription_interval".to_string(),
|
||||
open_charge
|
||||
.subscription_interval
|
||||
.unwrap_or(PriceDuration::Monthly)
|
||||
.as_str()
|
||||
.to_string(),
|
||||
);
|
||||
metadata.insert(
|
||||
"modrinth_charge_type".to_string(),
|
||||
ChargeType::Proration.as_str().to_string(),
|
||||
);
|
||||
|
||||
intent.customer = Some(customer_id);
|
||||
intent.metadata = Some(metadata);
|
||||
intent.receipt_email = user.email.as_deref();
|
||||
intent.setup_future_usage =
|
||||
Some(PaymentIntentSetupFutureUsage::OffSession);
|
||||
|
||||
if let Some(payment_method) =
|
||||
&edit_subscription.payment_method
|
||||
{
|
||||
let Ok(payment_method_id) =
|
||||
PaymentMethodId::from_str(payment_method)
|
||||
else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Invalid payment method id".to_string(),
|
||||
));
|
||||
};
|
||||
intent.payment_method = Some(payment_method_id);
|
||||
}
|
||||
|
||||
let intent =
|
||||
stripe::PaymentIntent::create(&stripe_client, intent)
|
||||
.await?;
|
||||
|
||||
Some((proration, 0, intent))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -947,14 +1069,17 @@ pub async fn active_servers(
|
||||
let server_ids = servers
|
||||
.into_iter()
|
||||
.filter_map(|x| {
|
||||
x.metadata.as_ref().map(|metadata| match metadata {
|
||||
SubscriptionMetadata::Pyro { id, region } => ActiveServer {
|
||||
user_id: x.user_id.into(),
|
||||
server_id: id.clone(),
|
||||
price_id: x.price_id.into(),
|
||||
interval: x.interval,
|
||||
region: region.clone(),
|
||||
},
|
||||
x.metadata.as_ref().and_then(|metadata| match metadata {
|
||||
SubscriptionMetadata::Pyro { id, region } => {
|
||||
Some(ActiveServer {
|
||||
user_id: x.user_id.into(),
|
||||
server_id: id.clone(),
|
||||
price_id: x.price_id.into(),
|
||||
interval: x.interval,
|
||||
region: region.clone(),
|
||||
})
|
||||
}
|
||||
SubscriptionMetadata::Medal { .. } => None,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<ActiveServer>>();
|
||||
@@ -1186,7 +1311,7 @@ pub async fn initiate_payment(
|
||||
})?;
|
||||
|
||||
let mut product_prices =
|
||||
product_item::DBProductPrice::get_all_product_prices(
|
||||
product_item::DBProductPrice::get_all_public_product_prices(
|
||||
product.id, &**pool,
|
||||
)
|
||||
.await?;
|
||||
@@ -1224,36 +1349,38 @@ pub async fn initiate_payment(
|
||||
}
|
||||
};
|
||||
|
||||
if let Price::Recurring { .. } = price_item.prices
|
||||
&& product.unitary
|
||||
{
|
||||
let user_subscriptions =
|
||||
if let Price::Recurring { .. } = price_item.prices {
|
||||
if product.unitary {
|
||||
let user_subscriptions =
|
||||
user_subscription_item::DBUserSubscription::get_all_user(
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user_products = product_item::DBProductPrice::get_many(
|
||||
&user_subscriptions
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
x.status == SubscriptionStatus::Provisioned
|
||||
})
|
||||
.map(|x| x.price_id)
|
||||
.collect::<Vec<_>>(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
let user_products =
|
||||
product_item::DBProductPrice::get_many(
|
||||
&user_subscriptions
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
x.status
|
||||
== SubscriptionStatus::Provisioned
|
||||
})
|
||||
.map(|x| x.price_id)
|
||||
.collect::<Vec<_>>(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if user_products
|
||||
.into_iter()
|
||||
.any(|x| x.product_id == product.id)
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You are already subscribed to this product!"
|
||||
.to_string(),
|
||||
));
|
||||
if user_products
|
||||
.into_iter()
|
||||
.any(|x| x.product_id == product.id)
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You are already subscribed to this product!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1701,6 +1828,13 @@ pub async fn stripe_webhook(
|
||||
|
||||
// Provision subscription
|
||||
match metadata.product_item.metadata {
|
||||
// A payment shouldn't be processed for Medal subscriptions.
|
||||
ProductMetadata::Medal { .. } => {
|
||||
warn!(
|
||||
"A payment processed for a free subscription"
|
||||
);
|
||||
}
|
||||
|
||||
ProductMetadata::Midas => {
|
||||
let badges =
|
||||
metadata.user_item.badges | Badges::MIDAS;
|
||||
@@ -1830,6 +1964,7 @@ pub async fn stripe_webhook(
|
||||
"region": server_region,
|
||||
"source": source,
|
||||
"payment_interval": metadata.charge_item.subscription_interval.map(|x| match x {
|
||||
PriceDuration::FiveDays => 1,
|
||||
PriceDuration::Monthly => 1,
|
||||
PriceDuration::Quarterly => 3,
|
||||
PriceDuration::Yearly => 12,
|
||||
@@ -1876,10 +2011,32 @@ pub async fn stripe_webhook(
|
||||
}
|
||||
};
|
||||
|
||||
// If the next open charge is actually an expiring charge,
|
||||
// this means the subscription was promoted from a temporary
|
||||
// free subscription to a paid subscription.
|
||||
//
|
||||
// In this case, we need to modify this expiring charge to be the
|
||||
// next charge of the subscription, turn it into a normal open charge.
|
||||
//
|
||||
// Otherwise, if there *is* an open charge, the subscription was upgraded
|
||||
// and the just-processed payment was the proration charge. In this case,
|
||||
// the existing open charge must be updated to reflect the new product's price.
|
||||
//
|
||||
// If there are no open charges, the just-processed payment was a recurring
|
||||
// or initial subscription charge, and we need to create the next charge.
|
||||
if let Some(mut charge) = open_charge {
|
||||
charge.price_id = metadata.product_price_item.id;
|
||||
charge.amount = new_price as i64;
|
||||
|
||||
if charge.status == ChargeStatus::Expiring {
|
||||
charge.status = ChargeStatus::Open;
|
||||
charge.due = Utc::now()
|
||||
+ subscription.interval.duration();
|
||||
charge.payment_platform =
|
||||
PaymentPlatform::Stripe;
|
||||
charge.last_attempt = None;
|
||||
} else {
|
||||
charge.price_id =
|
||||
metadata.product_price_item.id;
|
||||
charge.amount = new_price as i64;
|
||||
}
|
||||
charge.upsert(&mut transaction).await?;
|
||||
} else if metadata.charge_item.status
|
||||
!= ChargeStatus::Cancelled
|
||||
@@ -2001,36 +2158,38 @@ pub async fn stripe_webhook(
|
||||
EventType::PaymentMethodAttached => {
|
||||
if let EventObject::PaymentMethod(payment_method) =
|
||||
event.data.object
|
||||
&& let Some(customer_id) =
|
||||
payment_method.customer.map(|x| x.id())
|
||||
{
|
||||
let customer = stripe::Customer::retrieve(
|
||||
&stripe_client,
|
||||
&customer_id,
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
if customer
|
||||
.invoice_settings
|
||||
.is_none_or(|x| x.default_payment_method.is_none())
|
||||
if let Some(customer_id) =
|
||||
payment_method.customer.map(|x| x.id())
|
||||
{
|
||||
stripe::Customer::update(
|
||||
let customer = stripe::Customer::retrieve(
|
||||
&stripe_client,
|
||||
&customer_id,
|
||||
UpdateCustomer {
|
||||
invoice_settings: Some(
|
||||
CustomerInvoiceSettings {
|
||||
default_payment_method: Some(
|
||||
payment_method.id.to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
if customer
|
||||
.invoice_settings
|
||||
.is_none_or(|x| x.default_payment_method.is_none())
|
||||
{
|
||||
stripe::Customer::update(
|
||||
&stripe_client,
|
||||
&customer_id,
|
||||
UpdateCustomer {
|
||||
invoice_settings: Some(
|
||||
CustomerInvoiceSettings {
|
||||
default_payment_method: Some(
|
||||
payment_method.id.to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2100,7 +2259,11 @@ pub async fn index_subscriptions(pool: PgPool, redis: RedisPool) {
|
||||
let mut transaction = pool.begin().await?;
|
||||
let mut clear_cache_users = Vec::new();
|
||||
|
||||
// If an active subscription has a canceled charge OR a failed charge more than two days ago, it should be cancelled
|
||||
// If an active subscription has:
|
||||
// - A canceled charge due now
|
||||
// - An expiring charge due now
|
||||
// - A failed charge more than two days ago
|
||||
// It should be unprovisioned.
|
||||
let all_charges = DBCharge::get_unprovision(&pool).await?;
|
||||
|
||||
let mut all_subscriptions =
|
||||
@@ -2196,33 +2359,37 @@ pub async fn index_subscriptions(pool: PgPool, redis: RedisPool) {
|
||||
|
||||
true
|
||||
}
|
||||
ProductMetadata::Pyro { .. } => {
|
||||
if let Some(SubscriptionMetadata::Pyro { id, region: _ }) =
|
||||
&subscription.metadata
|
||||
{
|
||||
let res = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"{}/modrinth/v0/servers/{}/suspend",
|
||||
dotenvy::var("ARCHON_URL")?,
|
||||
id
|
||||
))
|
||||
.header("X-Master-Key", dotenvy::var("PYRO_API_KEY")?)
|
||||
.json(&serde_json::json!({
|
||||
"reason": if charge.status == ChargeStatus::Cancelled {
|
||||
"cancelled"
|
||||
} else {
|
||||
"paymentfailed"
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if let Err(e) = res {
|
||||
warn!("Error suspending pyro server: {:?}", e);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
ProductMetadata::Pyro { .. }
|
||||
| ProductMetadata::Medal { .. } => 'server: {
|
||||
let server_id = match &subscription.metadata {
|
||||
Some(SubscriptionMetadata::Pyro { id, region: _ }) => {
|
||||
id
|
||||
}
|
||||
Some(SubscriptionMetadata::Medal { id }) => id,
|
||||
_ => break 'server true,
|
||||
};
|
||||
|
||||
let res = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"{}/modrinth/v0/servers/{}/suspend",
|
||||
dotenvy::var("ARCHON_URL")?,
|
||||
server_id
|
||||
))
|
||||
.header("X-Master-Key", dotenvy::var("PYRO_API_KEY")?)
|
||||
.json(&serde_json::json!({
|
||||
"reason": if charge.status == ChargeStatus::Cancelled || charge.status == ChargeStatus::Expiring {
|
||||
"cancelled"
|
||||
} else {
|
||||
"paymentfailed"
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if let Err(e) = res {
|
||||
warn!("Error suspending pyro server: {:?}", e);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
@@ -2247,6 +2414,20 @@ pub async fn index_subscriptions(pool: PgPool, redis: RedisPool) {
|
||||
.await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
// If an offer redeemal has been processing for over 5 minutes, it should be set pending.
|
||||
UserRedeemal::update_stuck_5_minutes(&pool).await?;
|
||||
|
||||
// If an offer redeemal is pending, try processing it.
|
||||
// Try processing it.
|
||||
let pending_redeemals = UserRedeemal::get_pending(&pool, 100).await?;
|
||||
for redeemal in pending_redeemals {
|
||||
if let Err(error) =
|
||||
try_process_user_redeemal(&pool, &redis, redeemal).await
|
||||
{
|
||||
warn!(%error, "Failed to process a redeemal.")
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), ApiError>(())
|
||||
};
|
||||
|
||||
@@ -2257,6 +2438,161 @@ pub async fn index_subscriptions(pool: PgPool, redis: RedisPool) {
|
||||
info!("Done indexing subscriptions");
|
||||
}
|
||||
|
||||
/// Attempts to process a user redeemal.
|
||||
///
|
||||
/// Returns `Ok` if the entry has been succesfully processed, or will not be processed.
|
||||
pub async fn try_process_user_redeemal(
|
||||
pool: &PgPool,
|
||||
redis: &RedisPool,
|
||||
mut user_redeemal: UserRedeemal,
|
||||
) -> Result<(), ApiError> {
|
||||
// Immediately update redeemal row
|
||||
user_redeemal.last_attempt = Some(Utc::now());
|
||||
user_redeemal.n_attempts += 1;
|
||||
user_redeemal.status = users_redeemals::Status::Processing;
|
||||
let updated = user_redeemal.update_status_if_pending(pool).await?;
|
||||
|
||||
if !updated {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let user_id = user_redeemal.user_id;
|
||||
|
||||
// Find the Medal product's price & metadata
|
||||
|
||||
let mut medal_products =
|
||||
product_item::QueryProductWithPrices::list_by_product_type(
|
||||
pool, "medal",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some(product_item::QueryProductWithPrices {
|
||||
id: _product_id,
|
||||
metadata,
|
||||
mut prices,
|
||||
unitary: _,
|
||||
}) = medal_products.pop()
|
||||
else {
|
||||
return Err(ApiError::Conflict(
|
||||
"Missing Medal subscription product".to_owned(),
|
||||
));
|
||||
};
|
||||
|
||||
let ProductMetadata::Medal {
|
||||
cpu,
|
||||
ram,
|
||||
swap,
|
||||
storage,
|
||||
region,
|
||||
} = metadata
|
||||
else {
|
||||
return Err(ApiError::Conflict(
|
||||
"Missing or incorrect metadata for Medal subscription".to_owned(),
|
||||
));
|
||||
};
|
||||
|
||||
let Some(medal_price) = prices.pop() else {
|
||||
return Err(ApiError::Conflict(
|
||||
"Missing price for Medal subscription".to_owned(),
|
||||
));
|
||||
};
|
||||
|
||||
let (price_duration, price_amount) = match medal_price.prices {
|
||||
Price::OneTime { price: _ } => {
|
||||
return Err(ApiError::Conflict(
|
||||
"Unexpected metadata for Medal subscription price".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
Price::Recurring { intervals } => {
|
||||
let Some((price_duration, price_amount)) =
|
||||
intervals.into_iter().next()
|
||||
else {
|
||||
return Err(ApiError::Conflict(
|
||||
"Missing price interval for Medal subscription".to_owned(),
|
||||
));
|
||||
};
|
||||
|
||||
(price_duration, price_amount)
|
||||
}
|
||||
};
|
||||
|
||||
let price_id = medal_price.id;
|
||||
|
||||
// Get the user's username
|
||||
|
||||
let user = DBUser::get_id(user_id, pool, redis)
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
|
||||
// Send the provision request to Archon. On failure, the redeemal will be "stuck" processing,
|
||||
// and moved back to pending by `index_subscriptions`.
|
||||
|
||||
let archon_client = ArchonClient::from_env()?;
|
||||
let server_id = archon_client
|
||||
.create_server(&CreateServerRequest {
|
||||
user_id: to_base62(user_id.0 as u64),
|
||||
name: format!("{}'s Medal server", user.username),
|
||||
specs: Specs {
|
||||
memory_mb: ram,
|
||||
cpu,
|
||||
swap_mb: swap,
|
||||
storage_mb: storage,
|
||||
},
|
||||
source: crate::util::archon::Empty::default(),
|
||||
region,
|
||||
tags: vec!["medal".to_owned()],
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut txn = pool.begin().await?;
|
||||
|
||||
// Build a subscription using this price ID.
|
||||
let subscription = DBUserSubscription {
|
||||
id: generate_user_subscription_id(&mut txn).await?,
|
||||
user_id,
|
||||
price_id,
|
||||
interval: PriceDuration::FiveDays,
|
||||
created: Utc::now(),
|
||||
status: SubscriptionStatus::Provisioned,
|
||||
metadata: Some(SubscriptionMetadata::Medal {
|
||||
id: server_id.to_string(),
|
||||
}),
|
||||
};
|
||||
|
||||
subscription.upsert(&mut txn).await?;
|
||||
|
||||
// Insert an expiring charge, `index_subscriptions` will unprovision the
|
||||
// subscription when expired.
|
||||
DBCharge {
|
||||
id: generate_charge_id(&mut txn).await?,
|
||||
user_id,
|
||||
price_id,
|
||||
amount: price_amount.into(),
|
||||
currency_code: medal_price.currency_code,
|
||||
status: ChargeStatus::Expiring,
|
||||
due: Utc::now() + price_duration.duration(),
|
||||
last_attempt: None,
|
||||
type_: ChargeType::Subscription,
|
||||
subscription_id: Some(subscription.id),
|
||||
subscription_interval: Some(subscription.interval),
|
||||
payment_platform: PaymentPlatform::None,
|
||||
payment_platform_id: None,
|
||||
parent_charge_id: None,
|
||||
net: None,
|
||||
}
|
||||
.upsert(&mut txn)
|
||||
.await?;
|
||||
|
||||
// Update `users_redeemal`, mark subscription as redeemed.
|
||||
user_redeemal.status = users_redeemals::Status::Processed;
|
||||
user_redeemal.update(&mut *txn).await?;
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn index_billing(
|
||||
stripe_client: stripe::Client,
|
||||
pool: PgPool,
|
||||
|
||||
@@ -79,12 +79,13 @@ impl TempUser {
|
||||
file_host: &Arc<dyn FileHost + Send + Sync>,
|
||||
redis: &RedisPool,
|
||||
) -> Result<crate::database::models::DBUserId, AuthenticationError> {
|
||||
if let Some(email) = &self.email
|
||||
&& crate::database::models::DBUser::get_by_email(email, client)
|
||||
if let Some(email) = &self.email {
|
||||
if crate::database::models::DBUser::get_by_email(email, client)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Err(AuthenticationError::DuplicateUser);
|
||||
{
|
||||
return Err(AuthenticationError::DuplicateUser);
|
||||
}
|
||||
}
|
||||
|
||||
let user_id =
|
||||
@@ -1268,19 +1269,19 @@ pub async fn delete_auth_provider(
|
||||
.update_user_id(user.id.into(), None, &mut transaction)
|
||||
.await?;
|
||||
|
||||
if delete_provider.provider != AuthProvider::PayPal
|
||||
&& let Some(email) = user.email
|
||||
{
|
||||
send_email(
|
||||
email,
|
||||
"Authentication method removed",
|
||||
&format!(
|
||||
"When logging into Modrinth, you can no longer log in using the {} authentication provider.",
|
||||
delete_provider.provider.as_str()
|
||||
),
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
if delete_provider.provider != AuthProvider::PayPal {
|
||||
if let Some(email) = user.email {
|
||||
send_email(
|
||||
email,
|
||||
"Authentication method removed",
|
||||
&format!(
|
||||
"When logging into Modrinth, you can no longer log in using the {} authentication provider.",
|
||||
delete_provider.provider.as_str()
|
||||
),
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
109
apps/labrinth/src/routes/internal/medal.rs
Normal file
109
apps/labrinth/src/routes/internal/medal.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use actix_web::{HttpResponse, post, web};
|
||||
use ariadne::ids::UserId;
|
||||
use chrono::Utc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::database::models::users_redeemals::{
|
||||
Offer, RedeemalLookupFields, Status, UserRedeemal,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::internal::billing::try_process_user_redeemal;
|
||||
use crate::util::guards::medal_key_guard;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::scope("medal").service(verify).service(redeem));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MedalQuery {
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[post("verify", guard = "medal_key_guard")]
|
||||
pub async fn verify(
|
||||
pool: web::Data<PgPool>,
|
||||
web::Query(MedalQuery { username }): web::Query<MedalQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let maybe_fields =
|
||||
RedeemalLookupFields::redeemal_status_by_username_and_offer(
|
||||
&**pool,
|
||||
&username,
|
||||
Offer::Medal,
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct VerifyResponse {
|
||||
user_id: UserId,
|
||||
redeemed: bool,
|
||||
}
|
||||
|
||||
match maybe_fields {
|
||||
None => Err(ApiError::NotFound),
|
||||
Some(fields) => Ok(HttpResponse::Ok().json(VerifyResponse {
|
||||
user_id: fields.user_id.into(),
|
||||
redeemed: fields.redeemal_status.is_some(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("redeem", guard = "medal_key_guard")]
|
||||
pub async fn redeem(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
web::Query(MedalQuery { username }): web::Query<MedalQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
// Check the offer hasn't been redeemed yet, then insert into the table.
|
||||
// In a transaction to avoid double inserts.
|
||||
|
||||
let mut txn = pool.begin().await?;
|
||||
|
||||
let maybe_fields =
|
||||
RedeemalLookupFields::redeemal_status_by_username_and_offer(
|
||||
&mut *txn,
|
||||
&username,
|
||||
Offer::Medal,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user_id = match maybe_fields {
|
||||
None => return Err(ApiError::NotFound),
|
||||
Some(fields) => {
|
||||
if fields.redeemal_status.is_some() {
|
||||
return Err(ApiError::Conflict(
|
||||
"User already redeemed this offer".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
fields.user_id
|
||||
}
|
||||
};
|
||||
|
||||
// Link user to offer redeemal.
|
||||
let mut redeemal = UserRedeemal {
|
||||
id: 0,
|
||||
user_id,
|
||||
offer: Offer::Medal,
|
||||
redeemed: Utc::now(),
|
||||
status: Status::Pending,
|
||||
last_attempt: None,
|
||||
n_attempts: 0,
|
||||
};
|
||||
|
||||
redeemal.insert(&mut *txn).await?;
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
// Immediately try to process the redeemal
|
||||
if let Err(error) = try_process_user_redeemal(&pool, &redis, redeemal).await
|
||||
{
|
||||
warn!(%error, "Medal redeemal processing failed");
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
} else {
|
||||
Ok(HttpResponse::Created().finish())
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ pub(crate) mod admin;
|
||||
pub mod billing;
|
||||
pub mod flows;
|
||||
pub mod gdpr;
|
||||
pub mod medal;
|
||||
pub mod moderation;
|
||||
pub mod pats;
|
||||
pub mod session;
|
||||
@@ -24,6 +25,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
.configure(moderation::config)
|
||||
.configure(billing::config)
|
||||
.configure(gdpr::config)
|
||||
.configure(statuses::config),
|
||||
.configure(statuses::config)
|
||||
.configure(medal::config),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,16 +189,17 @@ pub async fn get_project_meta(
|
||||
.iter()
|
||||
.find(|x| Some(x.1.id as i32) == row.flame_project_id)
|
||||
.map(|x| x.0.clone())
|
||||
&& let Some(val) = merged.flame_files.remove(&sha1)
|
||||
{
|
||||
merged.identified.insert(
|
||||
sha1,
|
||||
IdentifiedFile {
|
||||
file_name: val.file_name.clone(),
|
||||
status: ApprovalType::from_string(&row.status)
|
||||
.unwrap_or(ApprovalType::Unidentified),
|
||||
},
|
||||
);
|
||||
if let Some(val) = merged.flame_files.remove(&sha1) {
|
||||
merged.identified.insert(
|
||||
sha1,
|
||||
IdentifiedFile {
|
||||
file_name: val.file_name.clone(),
|
||||
status: ApprovalType::from_string(&row.status)
|
||||
.unwrap_or(ApprovalType::Unidentified),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,69 +185,69 @@ pub async fn edit_pat(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(pat) = pat
|
||||
&& pat.user_id == user.id.into()
|
||||
{
|
||||
let mut transaction = pool.begin().await?;
|
||||
if let Some(pat) = pat {
|
||||
if pat.user_id == user.id.into() {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
if let Some(scopes) = &info.scopes {
|
||||
if scopes.is_restricted() {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Invalid scopes requested!".to_string(),
|
||||
));
|
||||
}
|
||||
if let Some(scopes) = &info.scopes {
|
||||
if scopes.is_restricted() {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Invalid scopes requested!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE pats
|
||||
SET scopes = $1
|
||||
WHERE id = $2
|
||||
",
|
||||
scopes.bits() as i64,
|
||||
pat.id.0
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
if let Some(name) = &info.name {
|
||||
sqlx::query!(
|
||||
"
|
||||
scopes.bits() as i64,
|
||||
pat.id.0
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
if let Some(name) = &info.name {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE pats
|
||||
SET name = $1
|
||||
WHERE id = $2
|
||||
",
|
||||
name,
|
||||
pat.id.0
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
if let Some(expires) = &info.expires {
|
||||
if expires < &Utc::now() {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Expire date must be in the future!".to_string(),
|
||||
));
|
||||
name,
|
||||
pat.id.0
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
if let Some(expires) = &info.expires {
|
||||
if expires < &Utc::now() {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Expire date must be in the future!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE pats
|
||||
SET expires = $1
|
||||
WHERE id = $2
|
||||
",
|
||||
expires,
|
||||
pat.id.0
|
||||
expires,
|
||||
pat.id.0
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::pat_item::DBPersonalAccessToken::clear_cache(
|
||||
vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))],
|
||||
&redis,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::pat_item::DBPersonalAccessToken::clear_cache(
|
||||
vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))],
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
@@ -276,21 +276,21 @@ pub async fn delete_pat(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(pat) = pat
|
||||
&& pat.user_id == user.id.into()
|
||||
{
|
||||
let mut transaction = pool.begin().await?;
|
||||
database::models::pat_item::DBPersonalAccessToken::remove(
|
||||
pat.id,
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
transaction.commit().await?;
|
||||
database::models::pat_item::DBPersonalAccessToken::clear_cache(
|
||||
vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))],
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
if let Some(pat) = pat {
|
||||
if pat.user_id == user.id.into() {
|
||||
let mut transaction = pool.begin().await?;
|
||||
database::models::pat_item::DBPersonalAccessToken::remove(
|
||||
pat.id,
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
transaction.commit().await?;
|
||||
database::models::pat_item::DBPersonalAccessToken::clear_cache(
|
||||
vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))],
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
|
||||
@@ -185,21 +185,21 @@ pub async fn delete(
|
||||
|
||||
let session = DBSession::get(info.into_inner().0, &**pool, &redis).await?;
|
||||
|
||||
if let Some(session) = session
|
||||
&& session.user_id == current_user.id.into()
|
||||
{
|
||||
let mut transaction = pool.begin().await?;
|
||||
DBSession::remove(session.id, &mut transaction).await?;
|
||||
transaction.commit().await?;
|
||||
DBSession::clear_cache(
|
||||
vec![(
|
||||
Some(session.id),
|
||||
Some(session.session),
|
||||
Some(session.user_id),
|
||||
)],
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
if let Some(session) = session {
|
||||
if session.user_id == current_user.id.into() {
|
||||
let mut transaction = pool.begin().await?;
|
||||
DBSession::remove(session.id, &mut transaction).await?;
|
||||
transaction.commit().await?;
|
||||
DBSession::clear_cache(
|
||||
vec![(
|
||||
Some(session.id),
|
||||
Some(session.session),
|
||||
Some(session.user_id),
|
||||
)],
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
|
||||
@@ -401,13 +401,14 @@ async fn broadcast_to_known_local_friends(
|
||||
friend.user_id
|
||||
};
|
||||
|
||||
if friend.accepted
|
||||
&& let Some(socket_ids) =
|
||||
if friend.accepted {
|
||||
if let Some(socket_ids) =
|
||||
sockets.sockets_by_user_id.get(&friend_id.into())
|
||||
{
|
||||
for socket_id in socket_ids.iter() {
|
||||
if let Some(socket) = sockets.sockets.get(&socket_id) {
|
||||
let _ = send_message(socket.value(), &message).await;
|
||||
{
|
||||
for socket_id in socket_ids.iter() {
|
||||
if let Some(socket) = sockets.sockets.get(&socket_id) {
|
||||
let _ = send_message(socket.value(), &message).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,8 @@ pub enum ApiError {
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Resource not found")]
|
||||
NotFound,
|
||||
#[error("Conflict: {0}")]
|
||||
Conflict(String),
|
||||
#[error(
|
||||
"You are being rate-limited. Please wait {0} milliseconds. 0/{1} remaining."
|
||||
)]
|
||||
@@ -172,6 +174,7 @@ impl ApiError {
|
||||
ApiError::Clickhouse(..) => "clickhouse_error",
|
||||
ApiError::Reroute(..) => "reroute_error",
|
||||
ApiError::NotFound => "not_found",
|
||||
ApiError::Conflict(..) => "conflict",
|
||||
ApiError::Zip(..) => "zip_error",
|
||||
ApiError::Io(..) => "io_error",
|
||||
ApiError::RateLimitError(..) => "ratelimit_error",
|
||||
@@ -208,6 +211,7 @@ impl actix_web::ResponseError for ApiError {
|
||||
ApiError::Mail(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Reroute(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::NotFound => StatusCode::NOT_FOUND,
|
||||
ApiError::Conflict(..) => StatusCode::CONFLICT,
|
||||
ApiError::Zip(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::Io(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::RateLimitError(..) => StatusCode::TOO_MANY_REQUESTS,
|
||||
|
||||
@@ -387,16 +387,17 @@ pub async fn revenue_get(
|
||||
.map(|x| (x.to_string(), HashMap::new()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
for value in payouts_values {
|
||||
if let Some(mod_id) = value.mod_id
|
||||
&& let Some(amount) = value.amount_sum
|
||||
&& let Some(interval_start) = value.interval_start
|
||||
{
|
||||
let id_string = to_base62(mod_id as u64);
|
||||
if !hm.contains_key(&id_string) {
|
||||
hm.insert(id_string.clone(), HashMap::new());
|
||||
}
|
||||
if let Some(hm) = hm.get_mut(&id_string) {
|
||||
hm.insert(interval_start.timestamp(), amount);
|
||||
if let Some(mod_id) = value.mod_id {
|
||||
if let Some(amount) = value.amount_sum {
|
||||
if let Some(interval_start) = value.interval_start {
|
||||
let id_string = to_base62(mod_id as u64);
|
||||
if !hm.contains_key(&id_string) {
|
||||
hm.insert(id_string.clone(), HashMap::new());
|
||||
}
|
||||
if let Some(hm) = hm.get_mut(&id_string) {
|
||||
hm.insert(interval_start.timestamp(), amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,10 +192,10 @@ pub async fn collection_get(
|
||||
.map(|x| x.1)
|
||||
.ok();
|
||||
|
||||
if let Some(data) = collection_data
|
||||
&& is_visible_collection(&data, &user_option, false).await?
|
||||
{
|
||||
return Ok(HttpResponse::Ok().json(Collection::from(data)));
|
||||
if let Some(data) = collection_data {
|
||||
if is_visible_collection(&data, &user_option, false).await? {
|
||||
return Ok(HttpResponse::Ok().json(Collection::from(data)));
|
||||
}
|
||||
}
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
|
||||
@@ -536,9 +536,11 @@ pub async fn create_payout(
|
||||
Some(true),
|
||||
)
|
||||
.await
|
||||
&& let Some(data) = res.items.first()
|
||||
{
|
||||
payout_item.platform_id = Some(data.payout_item_id.clone());
|
||||
if let Some(data) = res.items.first() {
|
||||
payout_item.platform_id =
|
||||
Some(data.payout_item_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -182,10 +182,10 @@ pub async fn project_get(
|
||||
.map(|x| x.1)
|
||||
.ok();
|
||||
|
||||
if let Some(data) = project_data
|
||||
&& is_visible_project(&data.inner, &user_option, &pool, false).await?
|
||||
{
|
||||
return Ok(HttpResponse::Ok().json(Project::from(data)));
|
||||
if let Some(data) = project_data {
|
||||
if is_visible_project(&data.inner, &user_option, &pool, false).await? {
|
||||
return Ok(HttpResponse::Ok().json(Project::from(data)));
|
||||
}
|
||||
}
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
@@ -403,36 +403,34 @@ pub async fn project_edit(
|
||||
.await?;
|
||||
}
|
||||
|
||||
if status.is_searchable()
|
||||
&& !project_item.inner.webhook_sent
|
||||
&& let Ok(webhook_url) = dotenvy::var("PUBLIC_DISCORD_WEBHOOK")
|
||||
{
|
||||
crate::util::webhook::send_discord_webhook(
|
||||
project_item.inner.id.into(),
|
||||
&pool,
|
||||
&redis,
|
||||
webhook_url,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
if status.is_searchable() && !project_item.inner.webhook_sent {
|
||||
if let Ok(webhook_url) = dotenvy::var("PUBLIC_DISCORD_WEBHOOK") {
|
||||
crate::util::webhook::send_discord_webhook(
|
||||
project_item.inner.id.into(),
|
||||
&pool,
|
||||
&redis,
|
||||
webhook_url,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET webhook_sent = TRUE
|
||||
WHERE id = $1
|
||||
",
|
||||
id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if user.role.is_mod()
|
||||
&& let Ok(webhook_url) = dotenvy::var("MODERATION_SLACK_WEBHOOK")
|
||||
{
|
||||
crate::util::webhook::send_slack_webhook(
|
||||
if user.role.is_mod() {
|
||||
if let Ok(webhook_url) = dotenvy::var("MODERATION_SLACK_WEBHOOK") {
|
||||
crate::util::webhook::send_slack_webhook(
|
||||
project_item.inner.id.into(),
|
||||
&pool,
|
||||
&redis,
|
||||
@@ -451,6 +449,7 @@ pub async fn project_edit(
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
if team_member.is_none_or(|x| !x.accepted) {
|
||||
@@ -693,45 +692,45 @@ pub async fn project_edit(
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(links) = &new_project.link_urls
|
||||
&& !links.is_empty()
|
||||
{
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
if let Some(links) = &new_project.link_urls {
|
||||
if !links.is_empty() {
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the links of this project!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let ids_to_delete = links.keys().cloned().collect::<Vec<String>>();
|
||||
// Deletes all links from hashmap- either will be deleted or be replaced
|
||||
sqlx::query!(
|
||||
"
|
||||
let ids_to_delete = links.keys().cloned().collect::<Vec<String>>();
|
||||
// Deletes all links from hashmap- either will be deleted or be replaced
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM mods_links
|
||||
WHERE joining_mod_id = $1 AND joining_platform_id IN (
|
||||
SELECT id FROM link_platforms WHERE name = ANY($2)
|
||||
)
|
||||
",
|
||||
id as db_ids::DBProjectId,
|
||||
&ids_to_delete
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
id as db_ids::DBProjectId,
|
||||
&ids_to_delete
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
for (platform, url) in links {
|
||||
if let Some(url) = url {
|
||||
let platform_id = db_models::categories::LinkPlatform::get_id(
|
||||
platform,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Platform {} does not exist.",
|
||||
platform.clone()
|
||||
))
|
||||
})?;
|
||||
sqlx::query!(
|
||||
for (platform, url) in links {
|
||||
if let Some(url) = url {
|
||||
let platform_id =
|
||||
db_models::categories::LinkPlatform::get_id(
|
||||
platform,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Platform {} does not exist.",
|
||||
platform.clone()
|
||||
))
|
||||
})?;
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO mods_links (joining_mod_id, joining_platform_id, url)
|
||||
VALUES ($1, $2, $3)
|
||||
@@ -742,6 +741,7 @@ pub async fn project_edit(
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2430,7 +2430,7 @@ pub async fn project_get_organization(
|
||||
organization,
|
||||
team_members,
|
||||
);
|
||||
Ok(HttpResponse::Ok().json(organization))
|
||||
return Ok(HttpResponse::Ok().json(organization));
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
|
||||
@@ -767,13 +767,12 @@ pub async fn edit_team_member(
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(new_permissions) = edit_member.permissions
|
||||
&& !permissions.contains(new_permissions)
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The new permissions have permissions that you don't have"
|
||||
.to_string(),
|
||||
));
|
||||
if let Some(new_permissions) = edit_member.permissions {
|
||||
if !permissions.contains(new_permissions) {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The new permissions have permissions that you don't have".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if edit_member.organization_permissions.is_some() {
|
||||
@@ -801,12 +800,13 @@ pub async fn edit_team_member(
|
||||
}
|
||||
|
||||
if let Some(new_permissions) = edit_member.organization_permissions
|
||||
&& !organization_permissions.contains(new_permissions)
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
if !organization_permissions.contains(new_permissions) {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The new organization permissions have permissions that you don't have"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if edit_member.permissions.is_some()
|
||||
@@ -822,13 +822,13 @@ pub async fn edit_team_member(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(payouts_split) = edit_member.payouts_split
|
||||
&& (payouts_split < Decimal::ZERO
|
||||
|| payouts_split > Decimal::from(5000))
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Payouts split must be between 0 and 5000!".to_string(),
|
||||
));
|
||||
if let Some(payouts_split) = edit_member.payouts_split {
|
||||
if payouts_split < Decimal::ZERO || payouts_split > Decimal::from(5000)
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Payouts split must be between 0 and 5000!".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
DBTeamMember::edit_team_member(
|
||||
@@ -883,13 +883,13 @@ pub async fn transfer_ownership(
|
||||
DBTeam::get_association(id.into(), &**pool).await?;
|
||||
if let Some(TeamAssociationId::Project(pid)) = team_association_id {
|
||||
let result = DBProject::get_id(pid, &**pool, &redis).await?;
|
||||
if let Some(project_item) = result
|
||||
&& project_item.inner.organization_id.is_some()
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
if let Some(project_item) = result {
|
||||
if project_item.inner.organization_id.is_some() {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You cannot transfer ownership of a project team that is owend by an organization"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -289,33 +289,36 @@ pub async fn thread_get(
|
||||
.await?
|
||||
.1;
|
||||
|
||||
if let Some(mut data) = thread_data
|
||||
&& is_authorized_thread(&data, &user, &pool).await?
|
||||
{
|
||||
let authors = &mut data.members;
|
||||
if let Some(mut data) = thread_data {
|
||||
if is_authorized_thread(&data, &user, &pool).await? {
|
||||
let authors = &mut data.members;
|
||||
|
||||
authors.append(
|
||||
&mut data
|
||||
.messages
|
||||
.iter()
|
||||
.filter_map(|x| {
|
||||
if x.hide_identity && !user.role.is_mod() {
|
||||
None
|
||||
} else {
|
||||
x.author_id
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
authors.append(
|
||||
&mut data
|
||||
.messages
|
||||
.iter()
|
||||
.filter_map(|x| {
|
||||
if x.hide_identity && !user.role.is_mod() {
|
||||
None
|
||||
} else {
|
||||
x.author_id
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
let users: Vec<User> =
|
||||
database::models::DBUser::get_many_ids(authors, &**pool, &redis)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(From::from)
|
||||
.collect();
|
||||
let users: Vec<User> = database::models::DBUser::get_many_ids(
|
||||
authors, &**pool, &redis,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(From::from)
|
||||
.collect();
|
||||
|
||||
return Ok(HttpResponse::Ok().json(Thread::from(data, users, &user)));
|
||||
return Ok(
|
||||
HttpResponse::Ok().json(Thread::from(data, users, &user))
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
@@ -451,32 +454,33 @@ pub async fn thread_send_message(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(project) = project
|
||||
&& project.inner.status != ProjectStatus::Processing
|
||||
&& user.role.is_mod()
|
||||
{
|
||||
let members =
|
||||
database::models::DBTeamMember::get_from_team_full(
|
||||
project.inner.team_id,
|
||||
&**pool,
|
||||
if let Some(project) = project {
|
||||
if project.inner.status != ProjectStatus::Processing
|
||||
&& user.role.is_mod()
|
||||
{
|
||||
let members =
|
||||
database::models::DBTeamMember::get_from_team_full(
|
||||
project.inner.team_id,
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::ModeratorMessage {
|
||||
thread_id: thread.id.into(),
|
||||
message_id: id.into(),
|
||||
project_id: Some(project.inner.id.into()),
|
||||
report_id: None,
|
||||
},
|
||||
}
|
||||
.insert_many(
|
||||
members.into_iter().map(|x| x.user_id).collect(),
|
||||
&mut transaction,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::ModeratorMessage {
|
||||
thread_id: thread.id.into(),
|
||||
message_id: id.into(),
|
||||
project_id: Some(project.inner.id.into()),
|
||||
report_id: None,
|
||||
},
|
||||
}
|
||||
.insert_many(
|
||||
members.into_iter().map(|x| x.user_id).collect(),
|
||||
&mut transaction,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
} else if let Some(report_id) = thread.report_id {
|
||||
let report = database::models::report_item::DBReport::get(
|
||||
|
||||
@@ -522,10 +522,10 @@ async fn version_create_inner(
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
if let Some(project_status) = project_status
|
||||
&& project_status.status == ProjectStatus::Processing.as_str()
|
||||
{
|
||||
moderation_queue.projects.insert(project_id.into());
|
||||
if let Some(project_status) = project_status {
|
||||
if project_status.status == ProjectStatus::Processing.as_str() {
|
||||
moderation_queue.projects.insert(project_id.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
@@ -871,16 +871,16 @@ pub async fn upload_file(
|
||||
ref format,
|
||||
ref files,
|
||||
} = validation_result
|
||||
&& dependencies.is_empty()
|
||||
{
|
||||
let hashes: Vec<Vec<u8>> = format
|
||||
.files
|
||||
.iter()
|
||||
.filter_map(|x| x.hashes.get(&PackFileHash::Sha1))
|
||||
.map(|x| x.as_bytes().to_vec())
|
||||
.collect();
|
||||
if dependencies.is_empty() {
|
||||
let hashes: Vec<Vec<u8>> = format
|
||||
.files
|
||||
.iter()
|
||||
.filter_map(|x| x.hashes.get(&PackFileHash::Sha1))
|
||||
.map(|x| x.as_bytes().to_vec())
|
||||
.collect();
|
||||
|
||||
let res = sqlx::query!(
|
||||
let res = sqlx::query!(
|
||||
"
|
||||
SELECT v.id version_id, v.mod_id project_id, h.hash hash FROM hashes h
|
||||
INNER JOIN files f on h.file_id = f.id
|
||||
@@ -892,44 +892,45 @@ pub async fn upload_file(
|
||||
.fetch_all(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
for file in &format.files {
|
||||
if let Some(dep) = res.iter().find(|x| {
|
||||
Some(&*x.hash)
|
||||
== file
|
||||
.hashes
|
||||
.get(&PackFileHash::Sha1)
|
||||
.map(|x| x.as_bytes())
|
||||
}) {
|
||||
dependencies.push(DependencyBuilder {
|
||||
project_id: Some(models::DBProjectId(dep.project_id)),
|
||||
version_id: Some(models::DBVersionId(dep.version_id)),
|
||||
file_name: None,
|
||||
dependency_type: DependencyType::Embedded.to_string(),
|
||||
});
|
||||
} else if let Some(first_download) = file.downloads.first() {
|
||||
dependencies.push(DependencyBuilder {
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
file_name: Some(
|
||||
first_download
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.unwrap_or(first_download)
|
||||
.to_string(),
|
||||
),
|
||||
dependency_type: DependencyType::Embedded.to_string(),
|
||||
});
|
||||
for file in &format.files {
|
||||
if let Some(dep) = res.iter().find(|x| {
|
||||
Some(&*x.hash)
|
||||
== file
|
||||
.hashes
|
||||
.get(&PackFileHash::Sha1)
|
||||
.map(|x| x.as_bytes())
|
||||
}) {
|
||||
dependencies.push(DependencyBuilder {
|
||||
project_id: Some(models::DBProjectId(dep.project_id)),
|
||||
version_id: Some(models::DBVersionId(dep.version_id)),
|
||||
file_name: None,
|
||||
dependency_type: DependencyType::Embedded.to_string(),
|
||||
});
|
||||
} else if let Some(first_download) = file.downloads.first() {
|
||||
dependencies.push(DependencyBuilder {
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
file_name: Some(
|
||||
first_download
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.unwrap_or(first_download)
|
||||
.to_string(),
|
||||
),
|
||||
dependency_type: DependencyType::Embedded.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for file in files {
|
||||
if !file.is_empty() {
|
||||
dependencies.push(DependencyBuilder {
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
file_name: Some(file.to_string()),
|
||||
dependency_type: DependencyType::Embedded.to_string(),
|
||||
});
|
||||
for file in files {
|
||||
if !file.is_empty() {
|
||||
dependencies.push(DependencyBuilder {
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
file_name: Some(file.to_string()),
|
||||
dependency_type: DependencyType::Embedded.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -973,10 +974,10 @@ pub async fn upload_file(
|
||||
));
|
||||
}
|
||||
|
||||
if let ValidationResult::Warning(msg) = validation_result
|
||||
&& primary
|
||||
{
|
||||
return Err(CreateError::InvalidInput(msg.to_string()));
|
||||
if let ValidationResult::Warning(msg) = validation_result {
|
||||
if primary {
|
||||
return Err(CreateError::InvalidInput(msg.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
let url = format!("{cdn_url}/{file_path_encode}");
|
||||
|
||||
@@ -148,55 +148,65 @@ pub async fn get_update_from_hash(
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
&& let Some(project) = database::models::DBProject::get_id(
|
||||
{
|
||||
if let Some(project) = database::models::DBProject::get_id(
|
||||
file.project_id,
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let mut versions = database::models::DBVersion::get_many(
|
||||
&project.versions,
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|x| {
|
||||
let mut bool = true;
|
||||
if let Some(version_types) = &update_data.version_types {
|
||||
bool &= version_types
|
||||
.iter()
|
||||
.any(|y| y.as_str() == x.inner.version_type);
|
||||
}
|
||||
if let Some(loaders) = &update_data.loaders {
|
||||
bool &= x.loaders.iter().any(|y| loaders.contains(y));
|
||||
}
|
||||
if let Some(loader_fields) = &update_data.loader_fields {
|
||||
for (key, values) in loader_fields {
|
||||
bool &= if let Some(x_vf) =
|
||||
x.version_fields.iter().find(|y| y.field_name == *key)
|
||||
{
|
||||
values.iter().any(|v| x_vf.value.contains_json_value(v))
|
||||
} else {
|
||||
true
|
||||
};
|
||||
{
|
||||
let mut versions = database::models::DBVersion::get_many(
|
||||
&project.versions,
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|x| {
|
||||
let mut bool = true;
|
||||
if let Some(version_types) = &update_data.version_types {
|
||||
bool &= version_types
|
||||
.iter()
|
||||
.any(|y| y.as_str() == x.inner.version_type);
|
||||
}
|
||||
}
|
||||
bool
|
||||
})
|
||||
.sorted();
|
||||
if let Some(loaders) = &update_data.loaders {
|
||||
bool &= x.loaders.iter().any(|y| loaders.contains(y));
|
||||
}
|
||||
if let Some(loader_fields) = &update_data.loader_fields {
|
||||
for (key, values) in loader_fields {
|
||||
bool &= if let Some(x_vf) = x
|
||||
.version_fields
|
||||
.iter()
|
||||
.find(|y| y.field_name == *key)
|
||||
{
|
||||
values
|
||||
.iter()
|
||||
.any(|v| x_vf.value.contains_json_value(v))
|
||||
} else {
|
||||
true
|
||||
};
|
||||
}
|
||||
}
|
||||
bool
|
||||
})
|
||||
.sorted();
|
||||
|
||||
if let Some(first) = versions.next_back() {
|
||||
if !is_visible_version(&first.inner, &user_option, &pool, &redis)
|
||||
if let Some(first) = versions.next_back() {
|
||||
if !is_visible_version(
|
||||
&first.inner,
|
||||
&user_option,
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
{
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
|
||||
return Ok(
|
||||
HttpResponse::Ok().json(models::projects::Version::from(first))
|
||||
);
|
||||
return Ok(HttpResponse::Ok()
|
||||
.json(models::projects::Version::from(first)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(ApiError::NotFound)
|
||||
@@ -388,12 +398,13 @@ pub async fn update_files(
|
||||
if let Some(version) = versions
|
||||
.iter()
|
||||
.find(|x| x.inner.project_id == file.project_id)
|
||||
&& let Some(hash) = file.hashes.get(&algorithm)
|
||||
{
|
||||
response.insert(
|
||||
hash.clone(),
|
||||
models::projects::Version::from(version.clone()),
|
||||
);
|
||||
if let Some(hash) = file.hashes.get(&algorithm) {
|
||||
response.insert(
|
||||
hash.clone(),
|
||||
models::projects::Version::from(version.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,59 +484,69 @@ pub async fn update_individual_files(
|
||||
|
||||
for project in projects {
|
||||
for file in files.iter().filter(|x| x.project_id == project.inner.id) {
|
||||
if let Some(hash) = file.hashes.get(&algorithm)
|
||||
&& let Some(query_file) =
|
||||
if let Some(hash) = file.hashes.get(&algorithm) {
|
||||
if let Some(query_file) =
|
||||
update_data.hashes.iter().find(|x| &x.hash == hash)
|
||||
{
|
||||
let version = all_versions
|
||||
.iter()
|
||||
.filter(|x| x.inner.project_id == file.project_id)
|
||||
.filter(|x| {
|
||||
let mut bool = true;
|
||||
|
||||
if let Some(version_types) = &query_file.version_types {
|
||||
bool &= version_types
|
||||
.iter()
|
||||
.any(|y| y.as_str() == x.inner.version_type);
|
||||
}
|
||||
if let Some(loaders) = &query_file.loaders {
|
||||
bool &=
|
||||
x.loaders.iter().any(|y| loaders.contains(y));
|
||||
}
|
||||
|
||||
if let Some(loader_fields) = &query_file.loader_fields {
|
||||
for (key, values) in loader_fields {
|
||||
bool &= if let Some(x_vf) = x
|
||||
.version_fields
|
||||
.iter()
|
||||
.find(|y| y.field_name == *key)
|
||||
{
|
||||
values.iter().any(|v| {
|
||||
x_vf.value.contains_json_value(v)
|
||||
})
|
||||
} else {
|
||||
true
|
||||
};
|
||||
}
|
||||
}
|
||||
bool
|
||||
})
|
||||
.sorted()
|
||||
.next_back();
|
||||
|
||||
if let Some(version) = version
|
||||
&& is_visible_version(
|
||||
&version.inner,
|
||||
&user_option,
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
response.insert(
|
||||
hash.clone(),
|
||||
models::projects::Version::from(version.clone()),
|
||||
);
|
||||
let version = all_versions
|
||||
.iter()
|
||||
.filter(|x| x.inner.project_id == file.project_id)
|
||||
.filter(|x| {
|
||||
let mut bool = true;
|
||||
|
||||
if let Some(version_types) =
|
||||
&query_file.version_types
|
||||
{
|
||||
bool &= version_types.iter().any(|y| {
|
||||
y.as_str() == x.inner.version_type
|
||||
});
|
||||
}
|
||||
if let Some(loaders) = &query_file.loaders {
|
||||
bool &= x
|
||||
.loaders
|
||||
.iter()
|
||||
.any(|y| loaders.contains(y));
|
||||
}
|
||||
|
||||
if let Some(loader_fields) =
|
||||
&query_file.loader_fields
|
||||
{
|
||||
for (key, values) in loader_fields {
|
||||
bool &= if let Some(x_vf) = x
|
||||
.version_fields
|
||||
.iter()
|
||||
.find(|y| y.field_name == *key)
|
||||
{
|
||||
values.iter().any(|v| {
|
||||
x_vf.value.contains_json_value(v)
|
||||
})
|
||||
} else {
|
||||
true
|
||||
};
|
||||
}
|
||||
}
|
||||
bool
|
||||
})
|
||||
.sorted()
|
||||
.next_back();
|
||||
|
||||
if let Some(version) = version {
|
||||
if is_visible_version(
|
||||
&version.inner,
|
||||
&user_option,
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
response.insert(
|
||||
hash.clone(),
|
||||
models::projects::Version::from(
|
||||
version.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,12 +106,13 @@ pub async fn version_project_get_helper(
|
||||
|| x.inner.version_number == id.1
|
||||
});
|
||||
|
||||
if let Some(version) = version
|
||||
&& is_visible_version(&version.inner, &user_option, &pool, &redis)
|
||||
if let Some(version) = version {
|
||||
if is_visible_version(&version.inner, &user_option, &pool, &redis)
|
||||
.await?
|
||||
{
|
||||
return Ok(HttpResponse::Ok()
|
||||
.json(models::projects::Version::from(version)));
|
||||
{
|
||||
return Ok(HttpResponse::Ok()
|
||||
.json(models::projects::Version::from(version)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,12 +190,12 @@ pub async fn version_get_helper(
|
||||
.map(|x| x.1)
|
||||
.ok();
|
||||
|
||||
if let Some(data) = version_data
|
||||
&& is_visible_version(&data.inner, &user_option, &pool, &redis).await?
|
||||
{
|
||||
return Ok(
|
||||
HttpResponse::Ok().json(models::projects::Version::from(data))
|
||||
);
|
||||
if let Some(data) = version_data {
|
||||
if is_visible_version(&data.inner, &user_option, &pool, &redis).await? {
|
||||
return Ok(
|
||||
HttpResponse::Ok().json(models::projects::Version::from(data))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Err(ApiError::NotFound)
|
||||
|
||||
@@ -15,12 +15,14 @@ pub async fn get_user_status(
|
||||
return Some(friend_status);
|
||||
}
|
||||
|
||||
if let Ok(mut conn) = redis.pool.get().await
|
||||
&& let Ok(mut statuses) =
|
||||
if let Ok(mut conn) = redis.pool.get().await {
|
||||
if let Ok(mut statuses) =
|
||||
conn.sscan::<_, String>(get_field_name(user)).await
|
||||
&& let Some(status_json) = statuses.next_item().await
|
||||
{
|
||||
return serde_json::from_str::<UserStatus>(&status_json).ok();
|
||||
{
|
||||
if let Some(status_json) = statuses.next_item().await {
|
||||
return serde_json::from_str::<UserStatus>(&status_json).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
|
||||
75
apps/labrinth/src/util/archon.rs
Normal file
75
apps/labrinth/src/util/archon.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use reqwest::header::HeaderName;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::routes::ApiError;
|
||||
|
||||
const X_MASTER_KEY: HeaderName = HeaderName::from_static("x-master-key");
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Empty {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Specs {
|
||||
pub memory_mb: u32,
|
||||
pub cpu: u32,
|
||||
pub swap_mb: u32,
|
||||
pub storage_mb: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateServerRequest {
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub specs: Specs,
|
||||
// Must be included because archon doesn't accept null values, only
|
||||
// an empty struct, as a source.
|
||||
pub source: Empty,
|
||||
pub region: String,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ArchonClient {
|
||||
client: reqwest::Client,
|
||||
base_url: String,
|
||||
pyro_api_key: String,
|
||||
}
|
||||
|
||||
impl ArchonClient {
|
||||
/// Builds an Archon client from environment variables. Returns `None` if the
|
||||
/// required environment variables are not set.
|
||||
pub fn from_env() -> Result<Self, ApiError> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let base_url =
|
||||
dotenvy::var("ARCHON_URL")?.trim_end_matches('/').to_owned();
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
base_url,
|
||||
pyro_api_key: dotenvy::var("PYRO_API_KEY")?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_server(
|
||||
&self,
|
||||
request: &CreateServerRequest,
|
||||
) -> Result<Uuid, reqwest::Error> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateServerResponse {
|
||||
uuid: Uuid,
|
||||
}
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(format!("{}/modrinth/v0/servers/create", self.base_url))
|
||||
.header(X_MASTER_KEY, &self.pyro_api_key)
|
||||
.json(request)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(response.json::<CreateServerResponse>().await?.uuid)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
use actix_web::guard::GuardContext;
|
||||
|
||||
pub const ADMIN_KEY_HEADER: &str = "Modrinth-Admin";
|
||||
pub const MEDAL_KEY_HEADER: &str = "X-Medal-Access-Key";
|
||||
|
||||
pub fn admin_key_guard(ctx: &GuardContext) -> bool {
|
||||
let admin_key = std::env::var("LABRINTH_ADMIN_KEY").expect(
|
||||
"No admin key provided, this should have been caught by check_env_vars",
|
||||
@@ -10,3 +12,16 @@ pub fn admin_key_guard(ctx: &GuardContext) -> bool {
|
||||
.get(ADMIN_KEY_HEADER)
|
||||
.is_some_and(|it| it.as_bytes() == admin_key.as_bytes())
|
||||
}
|
||||
|
||||
pub fn medal_key_guard(ctx: &GuardContext) -> bool {
|
||||
let maybe_medal_key = dotenvy::var("LABRINTH_MEDAL_KEY").ok();
|
||||
|
||||
match maybe_medal_key {
|
||||
None => false,
|
||||
Some(medal_key) => ctx
|
||||
.head()
|
||||
.headers()
|
||||
.get(MEDAL_KEY_HEADER)
|
||||
.is_some_and(|it| it.as_bytes() == medal_key.as_bytes()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,11 +138,12 @@ fn process_image(
|
||||
let (orig_width, orig_height) = img.dimensions();
|
||||
let aspect_ratio = orig_width as f32 / orig_height as f32;
|
||||
|
||||
if let Some(target_width) = target_width
|
||||
&& img.width() > target_width
|
||||
{
|
||||
let new_height = (target_width as f32 / aspect_ratio).round() as u32;
|
||||
img = img.resize(target_width, new_height, FilterType::Lanczos3);
|
||||
if let Some(target_width) = target_width {
|
||||
if img.width() > target_width {
|
||||
let new_height =
|
||||
(target_width as f32 / aspect_ratio).round() as u32;
|
||||
img = img.resize(target_width, new_height, FilterType::Lanczos3);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(min_aspect_ratio) = min_aspect_ratio {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod actix;
|
||||
pub mod archon;
|
||||
pub mod bitflag;
|
||||
pub mod captcha;
|
||||
pub mod cors;
|
||||
|
||||
@@ -133,11 +133,12 @@ pub async fn rate_limit_middleware(
|
||||
.expect("Rate limiter not configured properly")
|
||||
.clone();
|
||||
|
||||
if let Some(key) = req.headers().get("x-ratelimit-key")
|
||||
&& key.to_str().ok()
|
||||
if let Some(key) = req.headers().get("x-ratelimit-key") {
|
||||
if key.to_str().ok()
|
||||
== 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();
|
||||
|
||||
@@ -22,47 +22,46 @@ pub fn validation_errors_to_string(
|
||||
|
||||
let key_option = map.keys().next();
|
||||
|
||||
if let Some(field) = key_option
|
||||
&& let Some(error) = map.get(field)
|
||||
{
|
||||
return match error {
|
||||
ValidationErrorsKind::Struct(errors) => {
|
||||
validation_errors_to_string(
|
||||
*errors.clone(),
|
||||
Some(format!("of item {field}")),
|
||||
)
|
||||
}
|
||||
ValidationErrorsKind::List(list) => {
|
||||
if let Some((index, errors)) = list.iter().next() {
|
||||
output.push_str(&validation_errors_to_string(
|
||||
if let Some(field) = key_option {
|
||||
if let Some(error) = map.get(field) {
|
||||
return match error {
|
||||
ValidationErrorsKind::Struct(errors) => {
|
||||
validation_errors_to_string(
|
||||
*errors.clone(),
|
||||
Some(format!("of list {field} with index {index}")),
|
||||
));
|
||||
Some(format!("of item {field}")),
|
||||
)
|
||||
}
|
||||
ValidationErrorsKind::List(list) => {
|
||||
if let Some((index, errors)) = list.iter().next() {
|
||||
output.push_str(&validation_errors_to_string(
|
||||
*errors.clone(),
|
||||
Some(format!("of list {field} with index {index}")),
|
||||
));
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
ValidationErrorsKind::Field(errors) => {
|
||||
if let Some(error) = errors.first() {
|
||||
if let Some(adder) = adder {
|
||||
write!(
|
||||
output
|
||||
}
|
||||
ValidationErrorsKind::Field(errors) => {
|
||||
if let Some(error) = errors.first() {
|
||||
if let Some(adder) = adder {
|
||||
write!(
|
||||
&mut output,
|
||||
"Field {field} {adder} failed validation with error: {}",
|
||||
error.code
|
||||
).unwrap();
|
||||
} else {
|
||||
write!(
|
||||
&mut output,
|
||||
"Field {field} failed validation with error: {}",
|
||||
error.code
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
write!(
|
||||
&mut output,
|
||||
"Field {field} failed validation with error: {}",
|
||||
error.code
|
||||
).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
};
|
||||
output
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
String::new()
|
||||
|
||||
@@ -238,17 +238,17 @@ pub async fn send_slack_webhook(
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(icon_url) = metadata.project_icon_url
|
||||
&& let Some(project_block) = project_block.as_object_mut()
|
||||
{
|
||||
project_block.insert(
|
||||
"accessory".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "image",
|
||||
"image_url": icon_url,
|
||||
"alt_text": metadata.project_title
|
||||
}),
|
||||
);
|
||||
if let Some(icon_url) = metadata.project_icon_url {
|
||||
if let Some(project_block) = project_block.as_object_mut() {
|
||||
project_block.insert(
|
||||
"accessory".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "image",
|
||||
"image_url": icon_url,
|
||||
"alt_text": metadata.project_title
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
blocks.push(project_block);
|
||||
|
||||
@@ -222,10 +222,10 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
resp.status().as_u16()
|
||||
));
|
||||
}
|
||||
if resp.status() == StatusCode::OK
|
||||
&& let Some(failure_json_check) = &self.failure_json_check
|
||||
{
|
||||
failure_json_check(&test::read_body_json(resp).await);
|
||||
if resp.status() == StatusCode::OK {
|
||||
if let Some(failure_json_check) = &self.failure_json_check {
|
||||
failure_json_check(&test::read_body_json(resp).await);
|
||||
}
|
||||
}
|
||||
|
||||
// Failure test- logged in on a non-team user
|
||||
@@ -246,10 +246,10 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
resp.status().as_u16()
|
||||
));
|
||||
}
|
||||
if resp.status() == StatusCode::OK
|
||||
&& let Some(failure_json_check) = &self.failure_json_check
|
||||
{
|
||||
failure_json_check(&test::read_body_json(resp).await);
|
||||
if resp.status() == StatusCode::OK {
|
||||
if let Some(failure_json_check) = &self.failure_json_check {
|
||||
failure_json_check(&test::read_body_json(resp).await);
|
||||
}
|
||||
}
|
||||
|
||||
// Failure test- logged in with EVERY non-relevant permission
|
||||
@@ -270,10 +270,10 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
resp.status().as_u16()
|
||||
));
|
||||
}
|
||||
if resp.status() == StatusCode::OK
|
||||
&& let Some(failure_json_check) = &self.failure_json_check
|
||||
{
|
||||
failure_json_check(&test::read_body_json(resp).await);
|
||||
if resp.status() == StatusCode::OK {
|
||||
if let Some(failure_json_check) = &self.failure_json_check {
|
||||
failure_json_check(&test::read_body_json(resp).await);
|
||||
}
|
||||
}
|
||||
|
||||
// Patch user's permissions to success permissions
|
||||
@@ -300,10 +300,10 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
resp.status().as_u16()
|
||||
));
|
||||
}
|
||||
if resp.status() == StatusCode::OK
|
||||
&& let Some(success_json_check) = &self.success_json_check
|
||||
{
|
||||
success_json_check(&test::read_body_json(resp).await);
|
||||
if resp.status() == StatusCode::OK {
|
||||
if let Some(success_json_check) = &self.success_json_check {
|
||||
success_json_check(&test::read_body_json(resp).await);
|
||||
}
|
||||
}
|
||||
|
||||
// If the remove_user flag is set, remove the user from the project
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
allow-dbg-in-tests = true
|
||||
msrv = "1.89.0"
|
||||
msrv = "1.88.0"
|
||||
|
||||
@@ -50,10 +50,10 @@ pub async fn parse_command(
|
||||
// We assume anything else is a filepath to an .mrpack file
|
||||
let path = PathBuf::from(command_string);
|
||||
let path = io::canonicalize(path)?;
|
||||
if let Some(ext) = path.extension()
|
||||
&& ext == "mrpack"
|
||||
{
|
||||
return Ok(CommandPayload::RunMRPack { path });
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "mrpack" {
|
||||
return Ok(CommandPayload::RunMRPack { path });
|
||||
}
|
||||
}
|
||||
emit_warning(&format!(
|
||||
"Invalid command, unrecognized filetype: {}",
|
||||
|
||||
@@ -106,13 +106,13 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
})?;
|
||||
|
||||
// removes the old installation of java
|
||||
if let Some(file) = archive.file_names().next()
|
||||
&& let Some(dir) = file.split('/').next()
|
||||
{
|
||||
let path = path.join(dir);
|
||||
if let Some(file) = archive.file_names().next() {
|
||||
if let Some(dir) = file.split('/').next() {
|
||||
let path = path.join(dir);
|
||||
|
||||
if path.exists() {
|
||||
io::remove_dir_all(path).await?;
|
||||
if path.exists() {
|
||||
io::remove_dir_all(path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,11 +54,11 @@ pub async fn remove_user(uuid: uuid::Uuid) -> crate::Result<()> {
|
||||
if let Some((uuid, user)) = users.remove(&uuid) {
|
||||
Credentials::remove(uuid, &state.pool).await?;
|
||||
|
||||
if user.active
|
||||
&& let Some((_, mut user)) = users.into_iter().next()
|
||||
{
|
||||
user.active = true;
|
||||
user.upsert(&state.pool).await?;
|
||||
if user.active {
|
||||
if let Some((_, mut user)) = users.into_iter().next() {
|
||||
user.active = true;
|
||||
user.upsert(&state.pool).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -221,14 +221,14 @@ async fn import_atlauncher_unmanaged(
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
|
||||
if let Some(ref project_id) = description.project_id
|
||||
&& let Some(ref version_id) = description.version_id
|
||||
{
|
||||
prof.linked_data = Some(LinkedData {
|
||||
project_id: project_id.clone(),
|
||||
version_id: version_id.clone(),
|
||||
locked: true,
|
||||
})
|
||||
if let Some(ref project_id) = description.project_id {
|
||||
if let Some(ref version_id) = description.version_id {
|
||||
prof.linked_data = Some(LinkedData {
|
||||
project_id: project_id.clone(),
|
||||
version_id: version_id.clone(),
|
||||
locked: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
prof.icon_path = description
|
||||
|
||||
@@ -383,18 +383,18 @@ pub async fn set_profile_information(
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
|
||||
if let Some(ref project_id) = description.project_id
|
||||
&& let Some(ref version_id) = description.version_id
|
||||
{
|
||||
prof.linked_data = Some(LinkedData {
|
||||
project_id: project_id.clone(),
|
||||
version_id: version_id.clone(),
|
||||
locked: if !ignore_lock {
|
||||
true
|
||||
} else {
|
||||
prof.linked_data.as_ref().is_none_or(|x| x.locked)
|
||||
},
|
||||
})
|
||||
if let Some(ref project_id) = description.project_id {
|
||||
if let Some(ref version_id) = description.version_id {
|
||||
prof.linked_data = Some(LinkedData {
|
||||
project_id: project_id.clone(),
|
||||
version_id: version_id.clone(),
|
||||
locked: if !ignore_lock {
|
||||
true
|
||||
} else {
|
||||
prof.linked_data.as_ref().is_none_or(|x| x.locked)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
prof.icon_path = description
|
||||
|
||||
@@ -149,12 +149,13 @@ pub async fn install_zipped_mrpack_files(
|
||||
let profile_path = profile_path.clone();
|
||||
async move {
|
||||
//TODO: Future update: prompt user for optional files in a modpack
|
||||
if let Some(env) = project.env
|
||||
&& env
|
||||
if let Some(env) = project.env {
|
||||
if env
|
||||
.get(&EnvType::Client)
|
||||
.is_some_and(|x| x == &SideType::Unsupported)
|
||||
{
|
||||
return Ok(());
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let file = fetch_mirrors(
|
||||
@@ -374,12 +375,12 @@ pub async fn remove_all_related_files(
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if let Some(metadata) = &project.metadata
|
||||
&& to_remove.contains(&metadata.project_id)
|
||||
{
|
||||
let path = profile_full_path.join(file_path);
|
||||
if path.exists() {
|
||||
io::remove_file(&path).await?;
|
||||
if let Some(metadata) = &project.metadata {
|
||||
if to_remove.contains(&metadata.project_id) {
|
||||
let path = profile_full_path.join(file_path);
|
||||
if path.exists() {
|
||||
io::remove_file(&path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,26 +337,28 @@ pub async fn update_project(
|
||||
)
|
||||
.await?
|
||||
.remove(project_path)
|
||||
&& let Some(update_version) = &file.update_version_id
|
||||
{
|
||||
let path = Profile::add_project_version(
|
||||
profile_path,
|
||||
update_version,
|
||||
&state.pool,
|
||||
&state.fetch_semaphore,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
if let Some(update_version) = &file.update_version_id {
|
||||
let path = Profile::add_project_version(
|
||||
profile_path,
|
||||
update_version,
|
||||
&state.pool,
|
||||
&state.fetch_semaphore,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if path != project_path {
|
||||
Profile::remove_project(profile_path, project_path).await?;
|
||||
if path != project_path {
|
||||
Profile::remove_project(profile_path, project_path).await?;
|
||||
}
|
||||
|
||||
if !skip_send_event.unwrap_or(false) {
|
||||
emit_profile(profile_path, ProfilePayloadType::Edited)
|
||||
.await?;
|
||||
}
|
||||
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
if !skip_send_event.unwrap_or(false) {
|
||||
emit_profile(profile_path, ProfilePayloadType::Edited).await?;
|
||||
}
|
||||
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
Err(crate::ErrorKind::InputError(
|
||||
@@ -477,10 +479,10 @@ pub async fn export_mrpack(
|
||||
let included_export_candidates = included_export_candidates
|
||||
.into_iter()
|
||||
.filter(|x| {
|
||||
if let Some(f) = PathBuf::from(x).file_name()
|
||||
&& f.to_string_lossy().starts_with(".DS_Store")
|
||||
{
|
||||
return false;
|
||||
if let Some(f) = PathBuf::from(x).file_name() {
|
||||
if f.to_string_lossy().starts_with(".DS_Store") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
|
||||
@@ -184,7 +184,6 @@ pub enum LoadingBarType {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[cfg(feature = "tauri")]
|
||||
pub struct LoadingPayload {
|
||||
pub event: LoadingBarType,
|
||||
pub loader_uuid: Uuid,
|
||||
@@ -193,7 +192,11 @@ pub struct LoadingPayload {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[cfg(feature = "tauri")]
|
||||
pub struct OfflinePayload {
|
||||
pub offline: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct WarningPayload {
|
||||
pub message: String,
|
||||
}
|
||||
@@ -217,14 +220,12 @@ pub enum CommandPayload {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[cfg(feature = "tauri")]
|
||||
pub struct ProcessPayload {
|
||||
pub profile_path_id: String,
|
||||
pub uuid: Uuid,
|
||||
pub event: ProcessPayloadType,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProcessPayloadType {
|
||||
@@ -233,13 +234,11 @@ pub enum ProcessPayloadType {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[cfg(feature = "tauri")]
|
||||
pub struct ProfilePayload {
|
||||
pub profile_path_id: String,
|
||||
#[serde(flatten)]
|
||||
pub event: ProfilePayloadType,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(tag = "event", rename_all = "snake_case")]
|
||||
pub enum ProfilePayloadType {
|
||||
@@ -258,16 +257,6 @@ pub enum ProfilePayloadType {
|
||||
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)]
|
||||
pub enum EventError {
|
||||
#[error("Event state was not properly initialized")]
|
||||
@@ -280,3 +269,13 @@ pub enum EventError {
|
||||
#[error("Tauri error: {0}")]
|
||||
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,15 +32,15 @@ pub fn get_class_paths(
|
||||
let mut cps = libraries
|
||||
.iter()
|
||||
.filter_map(|library| {
|
||||
if let Some(rules) = &library.rules
|
||||
&& !parse_rules(
|
||||
if let Some(rules) = &library.rules {
|
||||
if !parse_rules(
|
||||
rules,
|
||||
java_arch,
|
||||
&QuickPlayType::None,
|
||||
minecraft_updated,
|
||||
)
|
||||
{
|
||||
return None;
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if !library.include_in_classpath {
|
||||
@@ -504,10 +504,10 @@ pub async fn get_processor_main_class(
|
||||
let mut line = line.map_err(IOError::from)?;
|
||||
line.retain(|c| !c.is_whitespace());
|
||||
|
||||
if line.starts_with("Main-Class:")
|
||||
&& let Some(class) = line.split(':').nth(1)
|
||||
{
|
||||
return Ok(Some(class.to_string()));
|
||||
if line.starts_with("Main-Class:") {
|
||||
if let Some(class) = line.split(':').nth(1) {
|
||||
return Ok(Some(class.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -290,11 +290,12 @@ pub async fn download_libraries(
|
||||
loading_try_for_each_concurrent(
|
||||
stream::iter(libraries.iter())
|
||||
.map(Ok::<&Library, crate::Error>), None, loading_bar,loading_amount,num_files, None,|library| async move {
|
||||
if let Some(rules) = &library.rules
|
||||
&& !parse_rules(rules, java_arch, &QuickPlayType::None, minecraft_updated) {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !parse_rules(rules, java_arch, &QuickPlayType::None, minecraft_updated) {
|
||||
tracing::trace!("Skipped library {}", &library.name);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if !library.downloadable {
|
||||
tracing::trace!("Skipped non-downloadable library {}", &library.name);
|
||||
@@ -310,14 +311,15 @@ pub async fn download_libraries(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(d::minecraft::LibraryDownloads { artifact: Some(ref artifact), ..}) = library.downloads
|
||||
&& !artifact.url.is_empty(){
|
||||
if let Some(d::minecraft::LibraryDownloads { artifact: Some(ref artifact), ..}) = library.downloads {
|
||||
if !artifact.url.is_empty(){
|
||||
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore, &st.pool)
|
||||
.await?;
|
||||
write(&path, &bytes, &st.io_semaphore).await?;
|
||||
tracing::trace!("Fetched library {} to path {:?}", &library.name, &path);
|
||||
return Ok::<_, crate::Error>(());
|
||||
}
|
||||
}
|
||||
|
||||
let url = [
|
||||
library
|
||||
|
||||
@@ -341,10 +341,10 @@ pub async fn install_minecraft(
|
||||
|
||||
// Forge processors (90-100)
|
||||
for (index, processor) in processors.iter().enumerate() {
|
||||
if let Some(sides) = &processor.sides
|
||||
&& !sides.contains(&String::from("client"))
|
||||
{
|
||||
continue;
|
||||
if let Some(sides) = &processor.sides {
|
||||
if !sides.contains(&String::from("client")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let cp = {
|
||||
|
||||
@@ -385,10 +385,10 @@ impl DirectoryInfo {
|
||||
return Err(e);
|
||||
}
|
||||
} else {
|
||||
if let Some(disk_usage) = get_disk_usage(&move_dir)?
|
||||
&& 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());
|
||||
if let Some(disk_usage) = get_disk_usage(&move_dir)? {
|
||||
if 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());
|
||||
}
|
||||
}
|
||||
|
||||
let loader_bar_id = Arc::new(&loader_bar_id);
|
||||
|
||||
@@ -9,7 +9,7 @@ use ariadne::networking::message::{
|
||||
ClientToServerMessage, ServerToClientMessage,
|
||||
};
|
||||
use ariadne::users::UserStatus;
|
||||
use async_tungstenite::WebSocketSender;
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use async_tungstenite::tokio::{ConnectStream, connect_async};
|
||||
use async_tungstenite::tungstenite::Message;
|
||||
use async_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
@@ -17,6 +17,7 @@ use bytes::Bytes;
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use either::Either;
|
||||
use futures::stream::SplitSink;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use reqwest::Method;
|
||||
use reqwest::header::HeaderValue;
|
||||
@@ -31,7 +32,7 @@ use tokio::sync::{Mutex, RwLock};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(super) type WriteSocket =
|
||||
Arc<RwLock<Option<WebSocketSender<ConnectStream>>>>;
|
||||
Arc<RwLock<Option<SplitSink<WebSocketStream<ConnectStream>, Message>>>>;
|
||||
pub(super) type TunnelSockets = Arc<DashMap<Uuid, Arc<InternalTunnelSocket>>>;
|
||||
|
||||
pub struct FriendsSocket {
|
||||
@@ -179,24 +180,27 @@ impl FriendsSocket {
|
||||
ServerToClientMessage::FriendSocketStoppedListening { .. } => {}, // TODO
|
||||
|
||||
ServerToClientMessage::SocketConnected { to_socket, new_socket } => {
|
||||
if let Some(connected_to) = sockets.get(&to_socket)
|
||||
&& let InternalTunnelSocket::Listening(local_addr) = *connected_to.value().clone()
|
||||
&& let Ok(new_stream) = TcpStream::connect(local_addr).await {
|
||||
if let Some(connected_to) = sockets.get(&to_socket) {
|
||||
if let InternalTunnelSocket::Listening(local_addr) = *connected_to.value().clone() {
|
||||
if let Ok(new_stream) = TcpStream::connect(local_addr).await {
|
||||
let (read, write) = new_stream.into_split();
|
||||
sockets.insert(new_socket, Arc::new(InternalTunnelSocket::Connected(Mutex::new(write))));
|
||||
Self::socket_read_loop(write_handle.clone(), read, new_socket);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = Self::send_message(&write_handle, ClientToServerMessage::SocketClose { socket: new_socket }).await;
|
||||
},
|
||||
ServerToClientMessage::SocketClosed { socket } => {
|
||||
sockets.remove_if(&socket, |_, x| matches!(*x.clone(), InternalTunnelSocket::Connected(_)));
|
||||
},
|
||||
ServerToClientMessage::SocketData { socket, data } => {
|
||||
if let Some(mut socket) = sockets.get_mut(&socket)
|
||||
&& let InternalTunnelSocket::Connected(ref stream) = *socket.value_mut().clone() {
|
||||
if let Some(mut socket) = sockets.get_mut(&socket) {
|
||||
if let InternalTunnelSocket::Connected(ref stream) = *socket.value_mut().clone() {
|
||||
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 world = world.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(state) = State::get().await
|
||||
&& let Err(e) = attached_world_data::AttachedWorldData::remove_for_world(
|
||||
if let Ok(state) = State::get().await {
|
||||
if let Err(e) = attached_world_data::AttachedWorldData::remove_for_world(
|
||||
&profile_path_str,
|
||||
WorldType::Singleplayer,
|
||||
&world,
|
||||
@@ -109,6 +109,7 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
|
||||
).await {
|
||||
tracing::warn!("Failed to remove AttachedWorldData for '{world}': {e}")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Some(ProfilePayloadType::WorldUpdated { world })
|
||||
@@ -149,14 +150,14 @@ pub(crate) async fn watch_profiles_init(
|
||||
) {
|
||||
if let Ok(profiles_dir) = std::fs::read_dir(dirs.profiles_dir()) {
|
||||
for profile_dir in profiles_dir {
|
||||
if let Ok(file_name) = profile_dir.map(|x| x.file_name())
|
||||
&& let Some(file_name) = file_name.to_str()
|
||||
{
|
||||
if file_name.starts_with(".DS_Store") {
|
||||
continue;
|
||||
};
|
||||
if let Ok(file_name) = profile_dir.map(|x| x.file_name()) {
|
||||
if let Some(file_name) = file_name.to_str() {
|
||||
if file_name.starts_with(".DS_Store") {
|
||||
continue;
|
||||
};
|
||||
|
||||
watch_profile(file_name, watcher, dirs).await;
|
||||
watch_profile(file_name, watcher, dirs).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +76,10 @@ where
|
||||
.loaded_config_dir
|
||||
.clone()
|
||||
.and_then(|x| x.to_str().map(|x| x.to_string()))
|
||||
&& path != old_launcher_root_str
|
||||
{
|
||||
settings.custom_dir = Some(path);
|
||||
if path != old_launcher_root_str {
|
||||
settings.custom_dir = Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
settings.prev_custom_dir = Some(old_launcher_root_str.clone());
|
||||
@@ -135,27 +136,31 @@ where
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(device_token) = minecraft_auth.token
|
||||
&& let Ok(private_key) =
|
||||
if let Some(device_token) = minecraft_auth.token {
|
||||
if let Ok(private_key) =
|
||||
SigningKey::from_pkcs8_pem(&device_token.private_key)
|
||||
&& let Ok(uuid) = Uuid::parse_str(&device_token.id)
|
||||
{
|
||||
DeviceTokenPair {
|
||||
token: DeviceToken {
|
||||
issue_instant: device_token.token.issue_instant,
|
||||
not_after: device_token.token.not_after,
|
||||
token: device_token.token.token,
|
||||
display_claims: device_token.token.display_claims,
|
||||
},
|
||||
key: DeviceTokenKey {
|
||||
id: uuid,
|
||||
key: private_key,
|
||||
x: device_token.x,
|
||||
y: device_token.y,
|
||||
},
|
||||
{
|
||||
if let Ok(uuid) = Uuid::parse_str(&device_token.id) {
|
||||
DeviceTokenPair {
|
||||
token: DeviceToken {
|
||||
issue_instant: device_token.token.issue_instant,
|
||||
not_after: device_token.token.not_after,
|
||||
token: device_token.token.token,
|
||||
display_claims: device_token
|
||||
.token
|
||||
.display_claims,
|
||||
},
|
||||
key: DeviceTokenKey {
|
||||
id: uuid,
|
||||
key: private_key,
|
||||
x: device_token.x,
|
||||
y: device_token.y,
|
||||
},
|
||||
}
|
||||
.upsert(exec)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
.upsert(exec)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,93 +207,100 @@ where
|
||||
update_version,
|
||||
..
|
||||
} = project.metadata
|
||||
&& let Some(file) = version
|
||||
{
|
||||
if let Some(file) = version
|
||||
.files
|
||||
.iter()
|
||||
.find(|x| x.hashes.get("sha512") == Some(&sha512))
|
||||
&& let Some(sha1) = file.hashes.get("sha1")
|
||||
{
|
||||
if let Ok(metadata) = full_path.metadata() {
|
||||
let file_name = format!(
|
||||
"{}/{}",
|
||||
profile.path,
|
||||
path.replace('\\', "/")
|
||||
.replace(".disabled", "")
|
||||
);
|
||||
{
|
||||
if let Some(sha1) = file.hashes.get("sha1") {
|
||||
if let Ok(metadata) = full_path.metadata() {
|
||||
let file_name = format!(
|
||||
"{}/{}",
|
||||
profile.path,
|
||||
path.replace('\\', "/")
|
||||
.replace(".disabled", "")
|
||||
);
|
||||
|
||||
cached_entries.push(CacheValue::FileHash(
|
||||
CachedFileHash {
|
||||
path: file_name,
|
||||
size: metadata.len(),
|
||||
hash: sha1.clone(),
|
||||
project_type:
|
||||
ProjectType::get_from_parent_folder(
|
||||
&full_path,
|
||||
),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
cached_entries.push(CacheValue::File(CachedFile {
|
||||
hash: sha1.clone(),
|
||||
project_id: version.project_id.clone(),
|
||||
version_id: version.id.clone(),
|
||||
}));
|
||||
|
||||
if let Some(update_version) = update_version {
|
||||
let mod_loader: ModLoader =
|
||||
profile.metadata.loader.into();
|
||||
cached_entries.push(CacheValue::FileUpdate(
|
||||
CachedFileUpdate {
|
||||
hash: sha1.clone(),
|
||||
game_version: profile
|
||||
.metadata
|
||||
.game_version
|
||||
.clone(),
|
||||
loaders: vec![
|
||||
mod_loader.as_str().to_string(),
|
||||
],
|
||||
update_version_id: update_version
|
||||
.id
|
||||
.clone(),
|
||||
},
|
||||
));
|
||||
|
||||
cached_entries.push(CacheValue::Version(
|
||||
(*update_version).into(),
|
||||
));
|
||||
}
|
||||
|
||||
let members = members
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let user = User {
|
||||
id: x.user.id,
|
||||
username: x.user.username,
|
||||
avatar_url: x.user.avatar_url,
|
||||
bio: x.user.bio,
|
||||
created: x.user.created,
|
||||
role: x.user.role,
|
||||
badges: 0,
|
||||
};
|
||||
|
||||
cached_entries
|
||||
.push(CacheValue::User(user.clone()));
|
||||
|
||||
TeamMember {
|
||||
team_id: x.team_id,
|
||||
user,
|
||||
is_owner: x.role == "Owner",
|
||||
role: x.role,
|
||||
ordering: x.ordering,
|
||||
cached_entries.push(CacheValue::FileHash(
|
||||
CachedFileHash {
|
||||
path: file_name,
|
||||
size: metadata.len(),
|
||||
hash: sha1.clone(),
|
||||
project_type: ProjectType::get_from_parent_folder(&full_path),
|
||||
},
|
||||
));
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cached_entries.push(CacheValue::Team(members));
|
||||
cached_entries.push(CacheValue::File(
|
||||
CachedFile {
|
||||
hash: sha1.clone(),
|
||||
project_id: version.project_id.clone(),
|
||||
version_id: version.id.clone(),
|
||||
},
|
||||
));
|
||||
|
||||
cached_entries
|
||||
.push(CacheValue::Version((*version).into()));
|
||||
if let Some(update_version) = update_version {
|
||||
let mod_loader: ModLoader =
|
||||
profile.metadata.loader.into();
|
||||
cached_entries.push(
|
||||
CacheValue::FileUpdate(
|
||||
CachedFileUpdate {
|
||||
hash: sha1.clone(),
|
||||
game_version: profile
|
||||
.metadata
|
||||
.game_version
|
||||
.clone(),
|
||||
loaders: vec![
|
||||
mod_loader
|
||||
.as_str()
|
||||
.to_string(),
|
||||
],
|
||||
update_version_id:
|
||||
update_version.id.clone(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
cached_entries.push(CacheValue::Version(
|
||||
(*update_version).into(),
|
||||
));
|
||||
}
|
||||
|
||||
let members = members
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let user = User {
|
||||
id: x.user.id,
|
||||
username: x.user.username,
|
||||
avatar_url: x.user.avatar_url,
|
||||
bio: x.user.bio,
|
||||
created: x.user.created,
|
||||
role: x.user.role,
|
||||
badges: 0,
|
||||
};
|
||||
|
||||
cached_entries.push(CacheValue::User(
|
||||
user.clone(),
|
||||
));
|
||||
|
||||
TeamMember {
|
||||
team_id: x.team_id,
|
||||
user,
|
||||
is_owner: x.role == "Owner",
|
||||
role: x.role,
|
||||
ordering: x.ordering,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cached_entries.push(CacheValue::Team(members));
|
||||
|
||||
cached_entries.push(CacheValue::Version(
|
||||
(*version).into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,15 +332,16 @@ where
|
||||
.map(|x| x.id),
|
||||
groups: profile.metadata.groups,
|
||||
linked_data: profile.metadata.linked_data.and_then(|x| {
|
||||
if let Some(project_id) = x.project_id
|
||||
&& let Some(version_id) = x.version_id
|
||||
&& let Some(locked) = x.locked
|
||||
{
|
||||
return Some(LinkedData {
|
||||
project_id,
|
||||
version_id,
|
||||
locked,
|
||||
});
|
||||
if let Some(project_id) = x.project_id {
|
||||
if let Some(version_id) = x.version_id {
|
||||
if let Some(locked) = x.locked {
|
||||
return Some(LinkedData {
|
||||
project_id,
|
||||
version_id,
|
||||
locked,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
|
||||
@@ -393,9 +393,10 @@ impl Credentials {
|
||||
..
|
||||
},
|
||||
) = *err.raw
|
||||
&& (source.is_connect() || source.is_timeout())
|
||||
{
|
||||
return Ok(Some(creds));
|
||||
if source.is_connect() || source.is_timeout() {
|
||||
return Ok(Some(creds));
|
||||
}
|
||||
}
|
||||
|
||||
Err(err)
|
||||
@@ -639,31 +640,36 @@ impl DeviceTokenPair {
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
if let Some(x) = res
|
||||
&& let Ok(uuid) = Uuid::parse_str(&x.uuid)
|
||||
&& let Ok(private_key) = SigningKey::from_pkcs8_pem(&x.private_key)
|
||||
{
|
||||
return Ok(Some(Self {
|
||||
token: DeviceToken {
|
||||
issue_instant: Utc
|
||||
.timestamp_opt(x.issue_instant, 0)
|
||||
.single()
|
||||
.unwrap_or_else(Utc::now),
|
||||
not_after: Utc
|
||||
.timestamp_opt(x.not_after, 0)
|
||||
.single()
|
||||
.unwrap_or_else(Utc::now),
|
||||
token: x.token,
|
||||
display_claims: serde_json::from_value(x.display_claims)
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
key: DeviceTokenKey {
|
||||
id: uuid,
|
||||
key: private_key,
|
||||
x: x.x,
|
||||
y: x.y,
|
||||
},
|
||||
}));
|
||||
if let Some(x) = res {
|
||||
if let Ok(uuid) = Uuid::parse_str(&x.uuid) {
|
||||
if let Ok(private_key) =
|
||||
SigningKey::from_pkcs8_pem(&x.private_key)
|
||||
{
|
||||
return Ok(Some(Self {
|
||||
token: DeviceToken {
|
||||
issue_instant: Utc
|
||||
.timestamp_opt(x.issue_instant, 0)
|
||||
.single()
|
||||
.unwrap_or_else(Utc::now),
|
||||
not_after: Utc
|
||||
.timestamp_opt(x.not_after, 0)
|
||||
.single()
|
||||
.unwrap_or_else(Utc::now),
|
||||
token: x.token,
|
||||
display_claims: serde_json::from_value(
|
||||
x.display_claims,
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
key: DeviceTokenKey {
|
||||
id: uuid,
|
||||
key: private_key,
|
||||
x: x.x,
|
||||
y: x.y,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -718,7 +724,7 @@ const MICROSOFT_CLIENT_ID: &str = "00000000402b5328";
|
||||
const AUTH_REPLY_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
||||
const REQUESTED_SCOPE: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
||||
|
||||
pub struct RequestWithDate<T> {
|
||||
struct RequestWithDate<T> {
|
||||
pub date: DateTime<Utc>,
|
||||
pub value: T,
|
||||
}
|
||||
|
||||
@@ -360,17 +360,18 @@ impl Process {
|
||||
}
|
||||
|
||||
// Write the throwable if present
|
||||
if !current_content.is_empty()
|
||||
&& let Err(e) =
|
||||
if !current_content.is_empty() {
|
||||
if let Err(e) =
|
||||
Process::append_to_log_file(
|
||||
&log_path,
|
||||
¤t_content,
|
||||
)
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to write throwable to log file: {}",
|
||||
e
|
||||
);
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to write throwable to log file: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -428,13 +429,15 @@ impl Process {
|
||||
|
||||
if let Some(timestamp) =
|
||||
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,
|
||||
timestamp,
|
||||
message
|
||||
).await {
|
||||
tracing::error!("Failed to handle server join logging: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -442,29 +445,35 @@ impl Process {
|
||||
}
|
||||
Ok(Event::Text(mut e)) => {
|
||||
if in_message || in_throwable {
|
||||
if let Ok(text) = e.xml_content() {
|
||||
if let Ok(text) = e.unescape() {
|
||||
current_content.push_str(&text);
|
||||
}
|
||||
} else if !in_event
|
||||
&& !e.inplace_trim_end()
|
||||
&& !e.inplace_trim_start()
|
||||
&& let Ok(text) = e.xml_content()
|
||||
&& let Err(e) = Process::append_to_log_file(
|
||||
&log_path,
|
||||
&format!("{text}\n"),
|
||||
)
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to write to log file: {}",
|
||||
e
|
||||
);
|
||||
if let Ok(text) = e.unescape() {
|
||||
if let Err(e) = Process::append_to_log_file(
|
||||
&log_path,
|
||||
&format!("{text}\n"),
|
||||
) {
|
||||
tracing::error!(
|
||||
"Failed to write to log file: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Event::CData(e)) => {
|
||||
if (in_message || in_throwable)
|
||||
&& let Ok(text) = e.xml_content()
|
||||
{
|
||||
current_content.push_str(&text);
|
||||
if in_message || in_throwable {
|
||||
if let Ok(text) = e
|
||||
.escape()
|
||||
.map_err(|x| x.into())
|
||||
.and_then(|x| x.unescape())
|
||||
{
|
||||
current_content.push_str(&text);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
@@ -711,13 +720,16 @@ impl Process {
|
||||
let logs_folder = state.directories.profile_logs_dir(&profile_path);
|
||||
let log_path = logs_folder.join(LAUNCHER_LOG_PATH);
|
||||
|
||||
if log_path.exists()
|
||||
&& let Err(e) = Process::append_to_log_file(
|
||||
if log_path.exists() {
|
||||
if let Err(e) = Process::append_to_log_file(
|
||||
&log_path,
|
||||
&format!("\n# Process exited with status: {mc_exit_status}\n"),
|
||||
)
|
||||
{
|
||||
tracing::warn!("Failed to write exit status to log file: {}", e);
|
||||
) {
|
||||
tracing::warn!(
|
||||
"Failed to write exit status to log file: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = state.discord_rpc.clear_to_default(true).await;
|
||||
|
||||
@@ -595,8 +595,8 @@ impl Profile {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, semaphore, icon))]
|
||||
pub async fn set_icon(
|
||||
&mut self,
|
||||
pub async fn set_icon<'a>(
|
||||
&'a mut self,
|
||||
cache_dir: &Path,
|
||||
semaphore: &IoSemaphore,
|
||||
icon: bytes::Bytes,
|
||||
@@ -629,20 +629,21 @@ impl Profile {
|
||||
{
|
||||
let subdirectory =
|
||||
subdirectory.map_err(io::IOError::from)?.path();
|
||||
if subdirectory.is_file()
|
||||
&& let Some(file_name) = subdirectory
|
||||
if subdirectory.is_file() {
|
||||
if let Some(file_name) = subdirectory
|
||||
.file_name()
|
||||
.and_then(|x| x.to_str())
|
||||
{
|
||||
let file_size = subdirectory
|
||||
.metadata()
|
||||
.map_err(io::IOError::from)?
|
||||
.len();
|
||||
{
|
||||
let file_size = subdirectory
|
||||
.metadata()
|
||||
.map_err(io::IOError::from)?
|
||||
.len();
|
||||
|
||||
keys.push(format!(
|
||||
"{file_size}-{}/{folder}/{file_name}",
|
||||
profile.path
|
||||
));
|
||||
keys.push(format!(
|
||||
"{file_size}-{}/{folder}/{file_name}",
|
||||
profile.path
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -900,29 +901,30 @@ impl Profile {
|
||||
{
|
||||
let subdirectory =
|
||||
subdirectory.map_err(io::IOError::from)?.path();
|
||||
if subdirectory.is_file()
|
||||
&& let Some(file_name) =
|
||||
if subdirectory.is_file() {
|
||||
if let Some(file_name) =
|
||||
subdirectory.file_name().and_then(|x| x.to_str())
|
||||
{
|
||||
let file_size = subdirectory
|
||||
.metadata()
|
||||
.map_err(io::IOError::from)?
|
||||
.len();
|
||||
{
|
||||
let file_size = subdirectory
|
||||
.metadata()
|
||||
.map_err(io::IOError::from)?
|
||||
.len();
|
||||
|
||||
keys.push(InitialScanFile {
|
||||
path: format!(
|
||||
"{}/{folder}/{}",
|
||||
self.path,
|
||||
file_name.trim_end_matches(".disabled")
|
||||
),
|
||||
file_name: file_name.to_string(),
|
||||
project_type,
|
||||
size: file_size,
|
||||
cache_key: format!(
|
||||
"{file_size}-{}/{folder}/{file_name}",
|
||||
self.path
|
||||
),
|
||||
});
|
||||
keys.push(InitialScanFile {
|
||||
path: format!(
|
||||
"{}/{folder}/{}",
|
||||
self.path,
|
||||
file_name.trim_end_matches(".disabled")
|
||||
),
|
||||
file_name: file_name.to_string(),
|
||||
project_type,
|
||||
size: file_size,
|
||||
cache_key: format!(
|
||||
"{file_size}-{}/{folder}/{file_name}",
|
||||
self.path
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ where
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(bytes, semaphore))]
|
||||
pub async fn write(
|
||||
pub async fn write<'a>(
|
||||
path: &Path,
|
||||
bytes: &[u8],
|
||||
semaphore: &IoSemaphore,
|
||||
|
||||
@@ -191,21 +191,22 @@ async fn get_all_autoinstalled_jre_path() -> Result<HashSet<PathBuf>, JREError>
|
||||
let mut jre_paths = HashSet::new();
|
||||
let base_path = state.directories.java_versions_dir();
|
||||
|
||||
if base_path.is_dir()
|
||||
&& let Ok(dir) = std::fs::read_dir(base_path)
|
||||
{
|
||||
for entry in dir.flatten() {
|
||||
let file_path = entry.path().join("bin");
|
||||
if base_path.is_dir() {
|
||||
if let Ok(dir) = std::fs::read_dir(base_path) {
|
||||
for entry in dir.flatten() {
|
||||
let file_path = entry.path().join("bin");
|
||||
|
||||
if let Ok(contents) = std::fs::read_to_string(file_path.clone())
|
||||
{
|
||||
let entry = entry.path().join(contents);
|
||||
jre_paths.insert(entry);
|
||||
} else {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
if let Ok(contents) =
|
||||
std::fs::read_to_string(file_path.clone())
|
||||
{
|
||||
let file_path = file_path.join(JAVA_BIN);
|
||||
jre_paths.insert(file_path);
|
||||
let entry = entry.path().join(contents);
|
||||
jre_paths.insert(entry);
|
||||
} else {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let file_path = file_path.join(JAVA_BIN);
|
||||
jre_paths.insert(file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,20 +300,20 @@ pub async fn check_java_at_filepath(path: &Path) -> crate::Result<JavaVersion> {
|
||||
}
|
||||
|
||||
// Extract version info from it
|
||||
if let Some(arch) = java_arch
|
||||
&& let Some(version) = java_version
|
||||
{
|
||||
if let Ok(version) = extract_java_version(version) {
|
||||
let path = java.to_string_lossy().to_string();
|
||||
return Ok(JavaVersion {
|
||||
parsed_version: version,
|
||||
path,
|
||||
version: version.to_string(),
|
||||
architecture: arch.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(arch) = java_arch {
|
||||
if let Some(version) = java_version {
|
||||
if let Ok(version) = extract_java_version(version) {
|
||||
let path = java.to_string_lossy().to_string();
|
||||
return Ok(JavaVersion {
|
||||
parsed_version: version,
|
||||
path,
|
||||
version: version.to_string(),
|
||||
architecture: arch.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
return Err(JREError::InvalidJREVersion(version.to_owned()).into());
|
||||
return Err(JREError::InvalidJREVersion(version.to_owned()).into());
|
||||
}
|
||||
}
|
||||
|
||||
Err(JREError::FailedJavaCheck(java).into())
|
||||
|
||||
@@ -33,11 +33,12 @@ pub fn is_feature_supported_in(
|
||||
if part_version == part_first_release {
|
||||
continue;
|
||||
}
|
||||
if let Ok(part_version) = part_version.parse::<u32>()
|
||||
&& let Ok(part_first_release) = part_first_release.parse::<u32>()
|
||||
&& part_version > part_first_release
|
||||
{
|
||||
return true;
|
||||
if let Ok(part_version) = part_version.parse::<u32>() {
|
||||
if let Ok(part_first_release) = part_first_release.parse::<u32>() {
|
||||
if part_version > part_first_release {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.89.0"
|
||||
channel = "1.88.0"
|
||||
|
||||
Reference in New Issue
Block a user