FlameAnvil Project Sync (#481)

* FlameAnvil Project Sync

* Perm fixes

* Fix compile

* Fix clippy + run prepare
This commit is contained in:
Geometrically 2022-11-20 19:50:14 -07:00 committed by GitHub
parent 589761bfd9
commit f259d81249
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 2501 additions and 1493 deletions

2
.env
View File

@ -1,6 +1,8 @@
DEBUG=true
RUST_LOG=info,sqlx::query=warn
SENTRY_DSN=none
SITE_URL=https://modrinth.com
CDN_URL=https://staging-cdn.modrinth.com
LABRINTH_ADMIN_KEY=feedbeef

242
Cargo.lock generated
View File

@ -250,6 +250,15 @@ dependencies = [
"syn",
]
[[package]]
name = "addr2line"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
@ -413,6 +422,21 @@ dependencies = [
"thiserror",
]
[[package]]
name = "backtrace"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.13.1"
@ -579,6 +603,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "chunked_transfer"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "cipher"
version = "0.3.0"
@ -851,6 +881,16 @@ dependencies = [
"parking_lot_core",
]
[[package]]
name = "debugid"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"serde",
"uuid",
]
[[package]]
name = "derive_more"
version = "0.99.17"
@ -1144,6 +1184,12 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "gimli"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
[[package]]
name = "h2"
version = "0.3.15"
@ -1233,6 +1279,17 @@ dependencies = [
"digest 0.10.5",
]
[[package]]
name = "hostname"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
dependencies = [
"libc",
"match_cfg",
"winapi",
]
[[package]]
name = "http"
version = "0.2.8"
@ -1473,7 +1530,7 @@ dependencies = [
[[package]]
name = "labrinth"
version = "2.5.0"
version = "2.6.0"
dependencies = [
"actix",
"actix-cors",
@ -1502,6 +1559,8 @@ dependencies = [
"reqwest",
"rust-s3",
"rust_decimal",
"sentry",
"sentry-actix",
"serde",
"serde_json",
"serde_with",
@ -1634,6 +1693,12 @@ dependencies = [
"linked-hash-map",
]
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "matches"
version = "0.1.9"
@ -1698,6 +1763,16 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mime_guess"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minidom"
version = "0.15.0"
@ -1822,6 +1897,15 @@ dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.16.0"
@ -1895,6 +1979,17 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "os_info"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4750134fb6a5d49afc80777394ad5d95b04bc12068c6abb92fae8f43817270f"
dependencies = [
"log",
"serde",
"winapi",
]
[[package]]
name = "parking"
version = "2.0.0"
@ -2196,6 +2291,7 @@ dependencies = [
"js-sys",
"log",
"mime",
"mime_guess",
"native-tls",
"once_cell",
"percent-encoding",
@ -2281,6 +2377,12 @@ dependencies = [
"serde",
]
[[package]]
name = "rustc-demangle"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]]
name = "rustc_version"
version = "0.4.0"
@ -2397,6 +2499,101 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
[[package]]
name = "sentry"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a120fb5e8b7975736bf1fc57de380531e617a6a8f5a55d037bcea25a7f5e8371"
dependencies = [
"httpdate",
"native-tls",
"reqwest",
"sentry-backtrace",
"sentry-contexts",
"sentry-core",
"sentry-panic",
"tokio",
"ureq",
]
[[package]]
name = "sentry-actix"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b889eb376e04a7f3b61aee3ed158c90dac12671042e07b6f54452be099ba5db7"
dependencies = [
"actix-web",
"futures-util",
"sentry-core",
]
[[package]]
name = "sentry-backtrace"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ac56ff9aae25b024a5aad4f0242808dfde29161c82d183adce778338c6822ef"
dependencies = [
"backtrace",
"once_cell",
"regex",
"sentry-core",
]
[[package]]
name = "sentry-contexts"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "188506b08b5e64004c71b7a5edb34959083e6e1288fada3b8d18d0bc7449ce1e"
dependencies = [
"hostname",
"libc",
"os_info",
"rustc_version",
"sentry-core",
"uname",
]
[[package]]
name = "sentry-core"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff58433a7ad557b586a09c42c4298d5f3ddb0c777e1a79d950e510d7b93fce0e"
dependencies = [
"once_cell",
"rand",
"sentry-types",
"serde",
"serde_json",
]
[[package]]
name = "sentry-panic"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4145005d9b5c117132765c34e2cb33e9d24d16e73d7f3a357122b77fe3a3b815"
dependencies = [
"sentry-backtrace",
"sentry-core",
]
[[package]]
name = "sentry-types"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb30d75498a041005a774ec1b6b7d9589c5906d17ebaca338cb685dc92170f9b"
dependencies = [
"chrono",
"debugid",
"getrandom",
"hex",
"serde",
"serde_json",
"thiserror",
"time 0.3.16",
"url",
"uuid",
]
[[package]]
name = "serde"
version = "1.0.147"
@ -2973,12 +3170,30 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "uname"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
dependencies = [
"libc",
]
[[package]]
name = "unchecked-index"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.8"
@ -3024,6 +3239,20 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f"
dependencies = [
"base64",
"chunked_transfer",
"log",
"native-tls",
"once_cell",
"url",
]
[[package]]
name = "url"
version = "2.3.1"
@ -3033,6 +3262,7 @@ dependencies = [
"form_urlencoded",
"idna 0.3.0",
"percent-encoding",
"serde",
]
[[package]]
@ -3041,6 +3271,16 @@ version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]]
name = "uuid"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
dependencies = [
"getrandom",
"serde",
]
[[package]]
name = "validator"
version = "0.16.0"

View File

@ -1,6 +1,6 @@
[package]
name = "labrinth"
version = "2.5.0"
version = "2.6.0"
authors = ["geometrically <jai@modrinth.com>"]
edition = "2018"
license = "AGPL-3.0"
@ -29,7 +29,7 @@ lazy_static = "1.4.0"
meilisearch-sdk = "0.15.0"
rust-s3 = "0.32.3"
reqwest = { version = "0.11.12", features = ["json"] }
reqwest = { version = "0.11.12", features = ["json", "multipart"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
@ -66,4 +66,7 @@ env_logger = "0.9.1"
thiserror = "1.0.37"
sqlx = { version = "0.6.2", features = ["runtime-actix-rustls", "postgres", "chrono", "offline", "macros", "migrate", "decimal"] }
rust_decimal = { version = "1.26", features = ["serde-with-float", "serde-with-str"] }
rust_decimal = { version = "1.26", features = ["serde-with-float", "serde-with-str"] }
sentry = "0.28.0"
sentry-actix = "0.28.0"

View File

@ -0,0 +1,6 @@
-- Add migration script here
ALTER TABLE payouts_values ALTER amount TYPE numeric(40, 20);
ALTER TABLE users ADD COLUMN flame_anvil_key varchar(40) NULL;
ALTER TABLE mods ADD COLUMN flame_anvil_project integer NULL;
ALTER TABLE mods ADD COLUMN flame_anvil_user bigint REFERENCES users NULL;

File diff suppressed because it is too large Load Diff

View File

@ -129,6 +129,8 @@ impl ProjectBuilder {
slug: self.slug,
moderation_message: None,
moderation_message_body: None,
flame_anvil_project: None,
flame_anvil_user: None,
};
project_struct.insert(&mut *transaction).await?;
@ -203,6 +205,8 @@ pub struct Project {
pub slug: Option<String>,
pub moderation_message: Option<String>,
pub moderation_message_body: Option<String>,
pub flame_anvil_project: Option<i32>,
pub flame_anvil_user: Option<UserId>,
}
impl Project {
@ -267,7 +271,8 @@ impl Project {
updated, approved, status,
issues_url, source_url, wiki_url, discord_url, license_url,
team_id, client_side, server_side, license, slug,
moderation_message, moderation_message_body
moderation_message, moderation_message_body, flame_anvil_project,
flame_anvil_user
FROM mods
WHERE id = $1
",
@ -303,6 +308,8 @@ impl Project {
moderation_message: row.moderation_message,
moderation_message_body: row.moderation_message_body,
approved: row.approved,
flame_anvil_project: row.flame_anvil_project,
flame_anvil_user: row.flame_anvil_user.map(UserId),
}))
} else {
Ok(None)
@ -327,7 +334,8 @@ impl Project {
updated, approved, status,
issues_url, source_url, wiki_url, discord_url, license_url,
team_id, client_side, server_side, license, slug,
moderation_message, moderation_message_body
moderation_message, moderation_message_body, flame_anvil_project,
flame_anvil_user
FROM mods
WHERE id = ANY($1)
",
@ -361,6 +369,8 @@ impl Project {
moderation_message: m.moderation_message,
moderation_message_body: m.moderation_message_body,
approved: m.approved,
flame_anvil_project: m.flame_anvil_project,
flame_anvil_user: m.flame_anvil_user.map(UserId)
}))
})
.try_collect::<Vec<Project>>()
@ -639,7 +649,7 @@ impl Project {
m.updated updated, m.approved approved, m.status status,
m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,
m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user,
ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories,
ARRAY_AGG(DISTINCT v.id || ' |||| ' || v.date_published) filter (where v.id is not null) versions,
ARRAY_AGG(DISTINCT mg.image_url || ' |||| ' || mg.featured || ' |||| ' || mg.created || ' |||| ' || COALESCE(mg.title, ' ') || ' |||| ' || COALESCE(mg.description, ' ')) filter (where mg.image_url is not null) gallery,
@ -709,6 +719,8 @@ impl Project {
moderation_message: m.moderation_message,
moderation_message_body: m.moderation_message_body,
approved: m.approved,
flame_anvil_project: m.flame_anvil_project,
flame_anvil_user: m.flame_anvil_user.map(UserId),
},
project_type: m.project_type_name,
categories,
@ -826,7 +838,7 @@ impl Project {
m.updated updated, m.approved approved, m.status status,
m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,
m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user,
ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories,
ARRAY_AGG(DISTINCT v.id || ' |||| ' || v.date_published) filter (where v.id is not null) versions,
ARRAY_AGG(DISTINCT mg.image_url || ' |||| ' || mg.featured || ' |||| ' || mg.created || ' |||| ' || COALESCE(mg.title, ' ') || ' |||| ' || COALESCE(mg.description, ' ')) filter (where mg.image_url is not null) gallery,
@ -897,7 +909,9 @@ impl Project {
follows: m.follows,
moderation_message: m.moderation_message,
moderation_message_body: m.moderation_message_body,
approved: m.approved
approved: m.approved,
flame_anvil_project: m.flame_anvil_project,
flame_anvil_user: m.flame_anvil_user.map(UserId)
},
project_type: m.project_type_name,
categories,

View File

@ -160,7 +160,7 @@ impl TeamMember {
u.avatar_url avatar_url, u.username username, u.bio bio,
u.created created, u.role user_role, u.badges badges, u.balance balance,
u.payout_wallet payout_wallet, u.payout_wallet_type payout_wallet_type,
u.payout_address payout_address
u.payout_address payout_address, u.flame_anvil_key flame_anvil_key
FROM team_members tm
INNER JOIN users u ON u.id = tm.user_id
WHERE tm.team_id = $1
@ -191,7 +191,8 @@ impl TeamMember {
balance: m.balance,
payout_wallet: m.payout_wallet.map(|x| RecipientWallet::from_string(&x)),
payout_wallet_type: m.payout_wallet_type.map(|x| RecipientType::from_string(&x)),
payout_address: m.payout_address
payout_address: m.payout_address,
flame_anvil_key: m.flame_anvil_key,
},
payouts_split: m.payouts_split
})))
@ -228,7 +229,7 @@ impl TeamMember {
u.avatar_url avatar_url, u.username username, u.bio bio,
u.created created, u.role user_role, u.badges badges, u.balance balance,
u.payout_wallet payout_wallet, u.payout_wallet_type payout_wallet_type,
u.payout_address payout_address
u.payout_address payout_address, u.flame_anvil_key flame_anvil_key
FROM team_members tm
INNER JOIN users u ON u.id = tm.user_id
WHERE tm.team_id = ANY($1)
@ -260,7 +261,8 @@ impl TeamMember {
balance: m.balance,
payout_wallet: m.payout_wallet.map(|x| RecipientWallet::from_string(&x)),
payout_wallet_type: m.payout_wallet_type.map(|x| RecipientType::from_string(&x)),
payout_address: m.payout_address
payout_address: m.payout_address,
flame_anvil_key: m.flame_anvil_key,
},
payouts_split: m.payouts_split
})))
@ -517,15 +519,24 @@ impl TeamMember {
Ok(())
}
pub async fn delete<'a, 'b, E>(
pub async fn delete<'a, 'b>(
id: TeamId,
user_id: UserId,
executor: E,
) -> Result<(), super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), super::DatabaseError> {
sqlx::query!(
"
UPDATE mods
SET flame_anvil_user = NULL
WHERE (team_id = $1 AND flame_anvil_user = $2 )
",
id as TeamId,
user_id as UserId,
)
.execute(&mut *transaction)
.await?;
sqlx::query!(
"
DELETE FROM team_members
WHERE (team_id = $1 AND user_id = $2 AND NOT role = $3)
@ -534,16 +545,9 @@ impl TeamMember {
user_id as UserId,
crate::models::teams::OWNER_ROLE,
)
.execute(executor)
.execute(&mut *transaction)
.await?;
if result.rows_affected() != 1 {
return Err(super::DatabaseError::Other(format!(
"Deleting a member failed; {} rows deleted",
result.rows_affected()
)));
}
Ok(())
}

View File

@ -18,6 +18,7 @@ pub struct User {
pub payout_wallet: Option<RecipientWallet>,
pub payout_wallet_type: Option<RecipientType>,
pub payout_address: Option<String>,
pub flame_anvil_key: Option<String>,
}
impl User {
@ -63,7 +64,7 @@ impl User {
u.avatar_url, u.username, u.bio,
u.created, u.role, u.badges,
u.balance, u.payout_wallet, u.payout_wallet_type,
u.payout_address
u.payout_address, u.flame_anvil_key
FROM users u
WHERE u.id = $1
",
@ -93,6 +94,7 @@ impl User {
.payout_wallet_type
.map(|x| RecipientType::from_string(&x)),
payout_address: row.payout_address,
flame_anvil_key: row.flame_anvil_key,
}))
} else {
Ok(None)
@ -112,7 +114,7 @@ impl User {
u.avatar_url, u.username, u.bio,
u.created, u.role, u.badges,
u.balance, u.payout_wallet, u.payout_wallet_type,
u.payout_address
u.payout_address, u.flame_anvil_key
FROM users u
WHERE u.github_id = $1
",
@ -142,6 +144,7 @@ impl User {
.payout_wallet_type
.map(|x| RecipientType::from_string(&x)),
payout_address: row.payout_address,
flame_anvil_key: row.flame_anvil_key,
}))
} else {
Ok(None)
@ -161,7 +164,7 @@ impl User {
u.avatar_url, u.username, u.bio,
u.created, u.role, u.badges,
u.balance, u.payout_wallet, u.payout_wallet_type,
u.payout_address
u.payout_address, u.flame_anvil_key
FROM users u
WHERE LOWER(u.username) = LOWER($1)
",
@ -191,6 +194,7 @@ impl User {
.payout_wallet_type
.map(|x| RecipientType::from_string(&x)),
payout_address: row.payout_address,
flame_anvil_key: row.flame_anvil_key,
}))
} else {
Ok(None)
@ -214,7 +218,7 @@ impl User {
u.avatar_url, u.username, u.bio,
u.created, u.role, u.badges,
u.balance, u.payout_wallet, u.payout_wallet_type,
u.payout_address
u.payout_address, u.flame_anvil_key
FROM users u
WHERE u.id = ANY($1)
",
@ -241,6 +245,7 @@ impl User {
.payout_wallet_type
.map(|x| RecipientType::from_string(&x)),
payout_address: u.payout_address,
flame_anvil_key: u.flame_anvil_key,
}))
})
.try_collect::<Vec<User>>()

View File

@ -1,5 +1,6 @@
use crate::file_hosting::S3Host;
use crate::queue::download::DownloadQueue;
use crate::queue::flameanvil::FlameAnvilQueue;
use crate::queue::payouts::PayoutsQueue;
use crate::ratelimit::errors::ARError;
use crate::ratelimit::memory::{MemoryStore, MemoryStoreActor};
@ -41,6 +42,14 @@ async fn main() -> std::io::Result<()> {
error!("Some environment variables are missing!");
}
// DSN is from SENTRY_DSN env variable.
// Has no effect if not set.
let sentry = sentry::init(());
if sentry.is_enabled() {
info!("Enabled Sentry integration");
std::env::set_var("RUST_BACKTRACE", "1");
}
info!(
"Starting Labrinth on {}",
dotenvy::var("BIND_ADDR").unwrap()
@ -166,6 +175,8 @@ async fn main() -> std::io::Result<()> {
.to_string(),
};
let flame_anvil_queue = Arc::new(Mutex::new(FlameAnvilQueue::new()));
let store = MemoryStore::new();
info!("Starting Actix HTTP server!");
@ -220,7 +231,9 @@ async fn main() -> std::io::Result<()> {
.app_data(web::Data::new(search_config.clone()))
.app_data(web::Data::new(download_queue.clone()))
.app_data(web::Data::new(payouts_queue.clone()))
.app_data(flame_anvil_queue.clone())
.app_data(web::Data::new(ip_salt.clone()))
.wrap(sentry_actix::Sentry::new())
.configure(routes::v1_config)
.configure(routes::v2_config)
.service(routes::index_get)

View File

@ -89,6 +89,11 @@ pub struct Project {
/// A string of URLs to visual content featuring the project
pub gallery: Vec<GalleryItem>,
/// The project linked from FlameAnvil to sync with
pub flame_anvil_project: Option<i32>,
/// The user_id of the team member whose token
pub flame_anvil_user: Option<UserId>,
}
impl From<QueryProject> for Project {
@ -153,6 +158,8 @@ impl From<QueryProject> for Project {
created: x.created,
})
.collect(),
flame_anvil_project: m.flame_anvil_project,
flame_anvil_user: m.flame_anvil_user.map(|x| x.into()),
}
}
}

View File

@ -69,9 +69,16 @@ pub struct TeamMember {
impl TeamMember {
pub fn from(data: QueryTeamMember, override_permissions: bool) -> Self {
let has_flame_anvil_key = data.user.flame_anvil_key.is_some();
let mut user: User = data.user.into();
if !override_permissions {
user.has_flame_anvil_key = Some(has_flame_anvil_key);
}
Self {
team_id: data.team_id.into(),
user: data.user.into(),
user,
role: data.role,
permissions: if override_permissions {
None

View File

@ -47,6 +47,7 @@ pub struct User {
pub role: Role,
pub badges: Badges,
pub payout_data: Option<UserPayoutData>,
pub has_flame_anvil_key: Option<bool>,
}
#[derive(Serialize, Deserialize, Clone)]
@ -140,6 +141,7 @@ impl From<DBUser> for User {
role: Role::from_string(&data.role),
badges: data.badges,
payout_data: None,
has_flame_anvil_key: None,
}
}
}

250
src/queue/flameanvil.rs Normal file
View File

@ -0,0 +1,250 @@
use crate::database::models::categories::GameVersion;
use crate::file_hosting::FileHostingError;
use crate::routes::project_creation::CreateError;
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Deserialize, Debug, Clone)]
struct FlameGameVersionType {
id: i32,
slug: String,
}
#[derive(Deserialize, Debug, Clone)]
struct FlameGameVersion {
id: i32,
#[serde(rename = "gameVersionTypeID")]
game_version_type_id: i32,
name: String,
slug: String,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct FlameUploadFile {
changelog: String,
// always "markdown"
changelog_type: String,
display_name: String,
game_versions: Vec<i32>,
release_type: String,
// TODO: relations?
}
pub struct FlameAnvilQueue {
mod_loaders: Vec<FlameGameVersion>,
minecraft_versions: Vec<FlameGameVersion>,
last_updated: DateTime<Utc>,
}
pub struct UploadFile {
pub loaders: Vec<String>,
pub game_versions: Vec<String>,
pub display_name: String,
pub changelog: String,
pub version_type: String,
}
// Batches download transactions every thirty seconds
impl FlameAnvilQueue {
pub fn new() -> Self {
FlameAnvilQueue {
mod_loaders: vec![],
minecraft_versions: vec![],
last_updated: Utc::now() - Duration::days(365),
}
}
pub fn convert_game_versions_to_flame(
&self,
original: Vec<String>,
game_versions: &[GameVersion],
) -> Vec<i32> {
let mut og_to_flame = HashMap::new();
let mut last_visited = if self
.minecraft_versions
.last()
.map(|x| x.name.ends_with("-Snapshot"))
.unwrap_or_default()
{
None
} else {
self.minecraft_versions
.iter()
.rfind(|x| !x.name.ends_with("-Snapshot"))
.cloned()
};
for game_version in game_versions {
if let Some(flame_game_version) =
self.minecraft_versions.iter().find(|x| {
x.name
== if game_version.version.starts_with('b') {
game_version.version.replace('b', "Beta ")
} else {
game_version.version.clone()
}
})
{
last_visited = Some(flame_game_version.clone());
og_to_flame
.insert(&game_version.version, flame_game_version.id);
} else if let Some(last_visited) = &last_visited {
if game_version.major {
og_to_flame.insert(&game_version.version, last_visited.id);
} else {
let mut splits = last_visited.name.split('.');
let new_str = format!(
"{}.{}-Snapshot",
splits.next().unwrap_or_default(),
splits.next().unwrap_or_default()
);
if let Some(flame_game_version) = self
.minecraft_versions
.iter()
.find(|x| x.name == new_str)
{
og_to_flame.insert(
&game_version.version,
flame_game_version.id,
);
} else {
og_to_flame
.insert(&game_version.version, last_visited.id);
}
}
} else if let Some(first) = self.minecraft_versions.last() {
og_to_flame.insert(&game_version.version, first.id);
}
}
let mut new = Vec::new();
for x in original {
if let Some(value) = og_to_flame.get(&&x) {
new.push(*value);
}
}
new
}
#[allow(clippy::too_many_arguments)]
pub async fn upload_file(
&mut self,
api_token: &str,
project_id: i32,
upload_file: UploadFile,
game_versions: &[GameVersion],
file: Vec<u8>,
file_name: String,
mime_type: String,
) -> Result<i32, CreateError> {
if self.last_updated < (Utc::now() - Duration::minutes(30)) {
self.index(api_token).await.map_err(|_| {
CreateError::InvalidInput(
"Indexing metadata from FlameAnvil failed!".to_string(),
)
})?;
}
let mut loaders_converted = upload_file
.loaders
.into_iter()
.filter_map(|x| self.mod_loaders.iter().find(|y| y.slug == x))
.map(|x| x.id)
.collect::<Vec<i32>>();
let mut game_versions_converted = self.convert_game_versions_to_flame(
upload_file.game_versions,
game_versions,
);
loaders_converted.append(&mut game_versions_converted);
let file = reqwest::multipart::Part::bytes(file)
.file_name(file_name)
.mime_str(&mime_type)
.map_err(|_| {
CreateError::InvalidInput(
"Error while converting inputted file to multipart payload"
.to_string(),
)
})?;
let form = reqwest::multipart::Form::new()
.text(
"metadata",
serde_json::to_string(&FlameUploadFile {
changelog: upload_file.changelog,
changelog_type: "markdown".to_string(),
display_name: upload_file.display_name,
game_versions: loaders_converted,
release_type: upload_file.version_type,
})
.unwrap(),
)
.part("file", file);
#[derive(Deserialize)]
struct FileResponse {
id: i32,
}
let client = reqwest::Client::new();
let id = client.post(&*format!("https://minecraft.curseforge.com/api/projects/{project_id}/upload-file?token={api_token}"))
.multipart(form)
.send()
.await.map_err(|_| CreateError::FileHostingError(FileHostingError::S3Error("Error uploading file to FlameAnvil!".to_string())))?
.json::<FileResponse>()
.await.map_err(|_| CreateError::FileHostingError(FileHostingError::S3Error("Error deserializing uploaded file response from FlameAnvil!".to_string())))?;
Ok(id.id)
}
pub async fn index(
&mut self,
api_token: &str,
) -> Result<(), reqwest::Error> {
let (game_versions, game_version_types) = futures::future::try_join(
reqwest::get(format!("https://minecraft.curseforge.com/api/game/versions?token={api_token}")),
reqwest::get(format!("https://minecraft.curseforge.com/api/game/version-types?token={api_token}"))
).await?;
let (game_versions, game_version_types) = futures::future::try_join(
game_versions.json::<Vec<FlameGameVersion>>(),
game_version_types.json::<Vec<FlameGameVersionType>>(),
)
.await?;
let mod_loader_types = game_version_types
.iter()
.filter(|x| x.slug == *"modloader")
.map(|x| x.id)
.collect::<Vec<_>>();
let minecraft_types = game_version_types
.iter()
.filter(|x| x.slug.starts_with("minecraft"))
.map(|x| x.id)
.collect::<Vec<_>>();
let mod_loaders = game_versions
.iter()
.filter(|x| mod_loader_types.contains(&x.game_version_type_id))
.cloned()
.collect::<Vec<_>>();
let minecraft_versions = game_versions
.iter()
.filter(|x| minecraft_types.contains(&x.game_version_type_id))
.cloned()
.collect::<Vec<_>>();
self.mod_loaders = mod_loaders;
self.minecraft_versions = minecraft_versions;
Ok(())
}
}

View File

@ -1,2 +1,3 @@
pub mod download;
pub mod flameanvil;
pub mod payouts;

View File

@ -97,13 +97,18 @@ impl PayoutsQueue {
})?;
}
let fee = std::cmp::min(
std::cmp::max(
Decimal::ONE / Decimal::from(4),
(Decimal::from(2) / Decimal::ONE_HUNDRED) * payout.amount.value,
),
Decimal::from(20),
);
let fee = if payout.recipient_wallet == *"Venmo" {
Decimal::ONE / Decimal::from(4)
} else {
std::cmp::min(
std::cmp::max(
Decimal::ONE / Decimal::from(4),
(Decimal::from(2) / Decimal::ONE_HUNDRED)
* payout.amount.value,
),
Decimal::from(20),
)
};
payout.amount.value -= fee;
@ -170,9 +175,9 @@ impl PayoutsQueue {
}
// Calculate actual fee + refund if we took too big of a fee.
if let Some(res) = res.json::<PayoutsResponse>().await.ok() {
if let Ok(res) = res.json::<PayoutsResponse>().await {
if let Some(link) = res.links.first() {
if let Some(res) = client
if let Ok(res) = client
.get(&link.href)
.header(
"Authorization",
@ -184,9 +189,8 @@ impl PayoutsQueue {
)
.send()
.await
.ok()
{
if let Some(res) = res.json::<PayoutData>().await.ok() {
if let Ok(res) = res.json::<PayoutData>().await {
if let Some(data) = res.items.first() {
if (fee - data.payout_item_fee.value)
> Decimal::ZERO

View File

@ -278,6 +278,7 @@ pub async fn auth_callback(
payout_wallet: None,
payout_wallet_type: None,
payout_address: None,
flame_anvil_key: None,
}
.insert(&mut transaction)
.await?;

View File

@ -5,6 +5,7 @@ use crate::models::projects::{
DonationLink, License, ProjectId, ProjectStatus, SideType, VersionId,
};
use crate::models::users::UserId;
use crate::queue::flameanvil::FlameAnvilQueue;
use crate::routes::version_creation::InitialVersionData;
use crate::search::indexing::IndexingError;
use crate::util::auth::{get_user_from_headers, AuthenticationError};
@ -22,6 +23,7 @@ use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use std::sync::Arc;
use thiserror::Error;
use tokio::sync::Mutex;
use validator::Validate;
#[derive(Error, Debug)]
@ -255,6 +257,7 @@ pub async fn project_create(
mut payload: Multipart,
client: Data<PgPool>,
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
flame_anvil_queue: Data<Arc<Mutex<FlameAnvilQueue>>>,
) -> Result<HttpResponse, CreateError> {
let mut transaction = client.begin().await?;
let mut uploaded_files = Vec::new();
@ -264,6 +267,7 @@ pub async fn project_create(
&mut payload,
&mut transaction,
&***file_host,
&flame_anvil_queue,
&mut uploaded_files,
)
.await;
@ -320,6 +324,7 @@ pub async fn project_create_inner(
payload: &mut Multipart,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
file_host: &dyn FileHost,
flame_anvil_queue: &Mutex<FlameAnvilQueue>,
uploaded_files: &mut Vec<UploadedFile>,
) -> Result<HttpResponse, CreateError> {
// The base URL for files uploaded to backblaze
@ -562,6 +567,7 @@ pub async fn project_create_inner(
super::version_creation::upload_file(
&mut field,
file_host,
version_data.file_parts.len(),
uploaded_files,
&mut created_version.files,
&mut created_version.dependencies,
@ -575,6 +581,12 @@ pub async fn project_create_inner(
all_game_versions.clone(),
version_data.primary_file.is_some(),
version_data.primary_file.as_deref() == Some(name),
version_data.version_title.clone(),
version_data.version_body.clone().unwrap_or_default(),
version_data.release_channel.clone().to_string(),
flame_anvil_queue,
None,
None,
transaction,
)
.await?;
@ -787,6 +799,8 @@ pub async fn project_create_inner(
discord_url: project_builder.discord_url.clone(),
donation_urls: project_create_data.donation_urls.clone(),
gallery: gallery_urls,
flame_anvil_project: None,
flame_anvil_user: None,
};
let _project_id = project_builder.insert(&mut *transaction).await?;

View File

@ -1,6 +1,7 @@
use crate::database;
use crate::file_hosting::FileHost;
use crate::models;
use crate::models::ids::UserId;
use crate::models::projects::{
DonationLink, Project, ProjectId, ProjectStatus, SearchRequest, SideType,
};
@ -341,6 +342,18 @@ pub struct EditProject {
)]
#[validate(length(max = 65536))]
pub moderation_message_body: Option<Option<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
pub flame_anvil_user: Option<Option<UserId>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
pub flame_anvil_project: Option<Option<i32>>,
}
#[patch("{id}")]
@ -979,6 +992,92 @@ pub async fn project_edit(
.await?;
}
if let Some(project) = &new_project.flame_anvil_project {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the external syncing project!"
.to_string(),
));
}
if project_item.project_type == "modpack" {
return Err(ApiError::InvalidInput(
"This project syncing feature is not available for modpacks!"
.to_string(),
));
}
sqlx::query!(
"
UPDATE mods
SET flame_anvil_project = $1
WHERE (id = $2)
",
*project,
id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
}
if let Some(user_id) = &new_project.flame_anvil_user {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the syncing user for this project!"
.to_string(),
));
}
if project_item.project_type == "modpack" {
return Err(ApiError::InvalidInput(
"This project syncing feature is not available for modpacks!"
.to_string(),
));
}
if let Some(user_id) = user_id {
if user_id != &user.id && !user.role.is_admin() {
return Err(ApiError::InvalidInput(
"You may only set yourself as the syncing user!"
.to_string(),
));
}
let results = sqlx::query!(
"
SELECT EXISTS(
SELECT 1 FROM team_members
INNER JOIN users u on team_members.user_id = u.id AND u.flame_anvil_key IS NOT NULL
WHERE team_id = $1 AND user_id = $2 AND accepted = TRUE
)
",
project_item.inner.team_id as database::models::ids::TeamId,
database::models::ids::UserId::from(*user_id) as database::models::ids::UserId,
)
.fetch_one(&mut *transaction)
.await?;
if !results.exists.unwrap_or(true) {
return Err(ApiError::InvalidInput(
"The given user is not part of your team or does not have a syncing key added to their account!"
.to_string(),
));
}
}
sqlx::query!(
"
UPDATE mods
SET flame_anvil_user = $1
WHERE (id = $2)
",
user_id.map(|x| x.0 as i64),
id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
}
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {

View File

@ -572,6 +572,8 @@ pub async fn remove_team_member(
));
}
let mut transaction = pool.begin().await?;
if delete_member.accepted {
// Members other than the owner can either leave the team, or be
// removed by a member with the REMOVE_MEMBER permission.
@ -579,7 +581,7 @@ pub async fn remove_team_member(
|| (member.permissions.contains(Permissions::REMOVE_MEMBER)
&& member.accepted)
{
TeamMember::delete(id, user_id, &**pool).await?;
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to remove a member from this team".to_string(),
@ -592,13 +594,15 @@ pub async fn remove_team_member(
// This is a pending invite rather than a member, so the
// user being invited or team members with the MANAGE_INVITES
// permission can remove it.
TeamMember::delete(id, user_id, &**pool).await?;
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to cancel a team invite"
.to_string(),
));
}
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))

View File

@ -167,6 +167,13 @@ pub struct EditUser {
)]
#[validate]
pub payout_data: Option<Option<EditPayoutData>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
#[validate(length(min = 1, max = 40), regex = "RE_URL_SAFE")]
pub flame_anvil_key: Option<Option<String>>,
}
#[derive(Serialize, Deserialize, Validate)]
@ -216,10 +223,10 @@ pub async fn user_edit(
{
sqlx::query!(
"
UPDATE users
SET username = $1
WHERE (id = $2)
",
UPDATE users
SET username = $1
WHERE (id = $2)
",
username,
id as crate::database::models::ids::UserId,
)
@ -388,6 +395,33 @@ pub async fn user_edit(
}
}
if let Some(flame_anvil_key) = &new_user.flame_anvil_key {
if flame_anvil_key.is_none() {
sqlx::query!(
"
UPDATE mods
SET flame_anvil_user = NULL
WHERE (flame_anvil_user = $1)
",
id as crate::database::models::ids::UserId,
)
.execute(&mut *transaction)
.await?;
}
sqlx::query!(
"
UPDATE users
SET flame_anvil_key = $1
WHERE (id = $2)
",
flame_anvil_key.as_deref(),
id as crate::database::models::ids::UserId,
)
.execute(&mut *transaction)
.await?;
}
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {

View File

@ -1,5 +1,6 @@
use crate::file_hosting::FileHost;
use crate::models::projects::SearchRequest;
use crate::queue::flameanvil::FlameAnvilQueue;
use crate::routes::project_creation::{
project_create_inner, undo_uploads, CreateError,
};
@ -15,6 +16,7 @@ use actix_web::{get, post, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ResultSearchMod {
@ -119,6 +121,7 @@ pub async fn mod_create(
mut payload: Multipart,
client: Data<PgPool>,
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
flame_anvil_queue: Data<Arc<Mutex<FlameAnvilQueue>>>,
) -> Result<HttpResponse, CreateError> {
let mut transaction = client.begin().await?;
let mut uploaded_files = Vec::new();
@ -128,6 +131,7 @@ pub async fn mod_create(
&mut payload,
&mut transaction,
&***file_host,
&flame_anvil_queue,
&mut uploaded_files,
)
.await;

View File

@ -10,6 +10,7 @@ use crate::models::projects::{
VersionFile, VersionId, VersionType,
};
use crate::models::teams::Permissions;
use crate::queue::flameanvil::{FlameAnvilQueue, UploadFile};
use crate::routes::project_creation::{CreateError, UploadedFile};
use crate::util::auth::get_user_from_headers;
use crate::util::routes::read_from_field;
@ -18,11 +19,13 @@ use crate::validate::{validate_file, ValidationResult};
use actix::fut::ready;
use actix_multipart::{Field, Multipart};
use actix_web::web::Data;
use actix_web::{post, HttpRequest, HttpResponse};
use actix_web::{post, web, HttpRequest, HttpResponse};
use chrono::Utc;
use futures::stream::StreamExt;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use std::sync::Arc;
use tokio::sync::Mutex;
use validator::Validate;
#[derive(Serialize, Deserialize, Validate, Clone)]
@ -68,7 +71,8 @@ pub async fn version_create(
req: HttpRequest,
mut payload: Multipart,
client: Data<PgPool>,
file_host: Data<std::sync::Arc<dyn FileHost + Send + Sync>>,
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
flame_anvil_queue: Data<Arc<Mutex<FlameAnvilQueue>>>,
) -> Result<HttpResponse, CreateError> {
let mut transaction = client.begin().await?;
let mut uploaded_files = Vec::new();
@ -78,6 +82,7 @@ pub async fn version_create(
&mut payload,
&mut transaction,
&***file_host,
&flame_anvil_queue,
&mut uploaded_files,
)
.await;
@ -108,6 +113,7 @@ async fn version_create_inner(
payload: &mut Multipart,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
file_host: &dyn FileHost,
flame_anvil_queue: &Mutex<FlameAnvilQueue>,
uploaded_files: &mut Vec<UploadedFile>,
) -> Result<HttpResponse, CreateError> {
let cdn_url = dotenvy::var("CDN_URL")?;
@ -280,16 +286,29 @@ async fn version_create_inner(
let project_type = sqlx::query!(
"
SELECT name FROM project_types pt
INNER JOIN mods ON mods.project_type = pt.id
WHERE mods.id = $1
",
SELECT name FROM project_types pt
INNER JOIN mods ON mods.project_type = pt.id
WHERE mods.id = $1
",
version.project_id as models::ProjectId,
)
.fetch_one(&mut *transaction)
.await?
.name;
let flame_anvil_info = sqlx::query!(
"
SELECT m.flame_anvil_project, u.flame_anvil_key
FROM mods m
INNER JOIN users u ON m.flame_anvil_user = u.id
WHERE m.id = $1
",
version.project_id as models::ProjectId,
)
.fetch_optional(&mut *transaction)
.await?
.map(|x| (x.flame_anvil_project, x.flame_anvil_key));
let version_data = initial_version_data.clone().ok_or_else(|| {
CreateError::InvalidInput("`data` field is required".to_string())
})?;
@ -297,6 +316,7 @@ async fn version_create_inner(
upload_file(
&mut field,
file_host,
version_data.file_parts.len(),
uploaded_files,
&mut version.files,
&mut version.dependencies,
@ -310,6 +330,12 @@ async fn version_create_inner(
all_game_versions.clone(),
version_data.primary_file.is_some(),
version_data.primary_file.as_deref() == Some(name),
version_data.version_title.clone(),
version_data.version_body.clone().unwrap_or_default(),
version_data.release_channel.clone().to_string(),
flame_anvil_queue,
flame_anvil_info.clone().and_then(|x| x.0),
flame_anvil_info.and_then(|x| x.1),
transaction,
)
.await?;
@ -344,9 +370,9 @@ async fn version_create_inner(
let users = sqlx::query!(
"
SELECT follower_id FROM mod_follows
WHERE mod_id = $1
",
SELECT follower_id FROM mod_follows
WHERE mod_id = $1
",
builder.project_id as crate::database::models::ids::ProjectId
)
.fetch_many(&mut *transaction)
@ -363,7 +389,7 @@ async fn version_create_inner(
notification_type: Some("project_update".to_string()),
title: format!("**{}** has been updated!", result.title),
text: format!(
"The project, {}, has released a new version: {}",
"The project {} has released a new version: {}",
result.title,
version_data.version_number.clone()
),
@ -428,10 +454,11 @@ async fn version_create_inner(
#[post("{version_id}/file")]
pub async fn upload_file_to_version(
req: HttpRequest,
url_data: actix_web::web::Path<(VersionId,)>,
url_data: web::Path<(VersionId,)>,
mut payload: Multipart,
client: Data<PgPool>,
file_host: Data<std::sync::Arc<dyn FileHost + Send + Sync>>,
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
flame_anvil_queue: Data<Arc<Mutex<FlameAnvilQueue>>>,
) -> Result<HttpResponse, CreateError> {
let mut transaction = client.begin().await?;
let mut uploaded_files = Vec::new();
@ -444,6 +471,7 @@ pub async fn upload_file_to_version(
client,
&mut transaction,
&***file_host,
&flame_anvil_queue,
&mut uploaded_files,
version_id,
)
@ -470,12 +498,14 @@ pub async fn upload_file_to_version(
result
}
#[allow(clippy::too_many_arguments)]
async fn upload_file_to_version_inner(
req: HttpRequest,
payload: &mut Multipart,
client: Data<PgPool>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
file_host: &dyn FileHost,
flame_anvil_queue: &Mutex<FlameAnvilQueue>,
uploaded_files: &mut Vec<UploadedFile>,
version_id: models::VersionId,
) -> Result<HttpResponse, CreateError> {
@ -578,6 +608,7 @@ async fn upload_file_to_version_inner(
upload_file(
&mut field,
file_host,
0,
uploaded_files,
&mut file_builders,
&mut dependencies,
@ -596,6 +627,12 @@ async fn upload_file_to_version_inner(
all_game_versions.clone(),
true,
false,
version.name.clone(),
version.changelog.clone(),
version.version_type.clone(),
flame_anvil_queue,
None,
None,
transaction,
)
.await?;
@ -620,6 +657,7 @@ async fn upload_file_to_version_inner(
pub async fn upload_file(
field: &mut Field,
file_host: &dyn FileHost,
total_files_len: usize,
uploaded_files: &mut Vec<UploadedFile>,
version_files: &mut Vec<VersionFileBuilder>,
dependencies: &mut Vec<DependencyBuilder>,
@ -633,6 +671,12 @@ pub async fn upload_file(
all_game_versions: Vec<models::categories::GameVersion>,
ignore_primary: bool,
force_primary: bool,
version_display_name: String,
version_changelog: String,
version_type: String,
flame_anvil_queue: &Mutex<FlameAnvilQueue>,
flame_anvil_project: Option<i32>,
flame_anvil_key: Option<String>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), CreateError> {
let (file_name, file_extension) = get_name_ext(content_disposition)?;
@ -672,9 +716,9 @@ pub async fn upload_file(
data.clone().into(),
file_extension.to_string(),
project_type.to_string(),
loaders,
game_versions,
all_game_versions,
loaders.clone(),
game_versions.clone(),
all_game_versions.clone(),
)
.await?;
@ -745,6 +789,43 @@ pub async fn upload_file(
}
}
let data = data.freeze();
let primary = (validation_result.is_passed()
&& version_files.iter().all(|x| !x.primary)
&& !ignore_primary)
|| force_primary
|| total_files_len == 1;
if primary {
if let Some(project_id) = flame_anvil_project {
if let Some(key) = flame_anvil_key {
let mut flame_anvil_queue = flame_anvil_queue.lock().await;
flame_anvil_queue
.upload_file(
&key,
project_id,
UploadFile {
loaders: loaders.into_iter().map(|x| x.0).collect(),
game_versions: game_versions
.into_iter()
.map(|x| x.0)
.collect(),
display_name: version_display_name,
changelog: version_changelog,
version_type,
},
&all_game_versions,
data.to_vec(),
file_name.to_string(),
content_type.to_string(),
)
.await?;
}
}
}
let file_path_encode = format!(
"data/{}/versions/{}/{}",
project_id,
@ -755,7 +836,7 @@ pub async fn upload_file(
format!("data/{}/versions/{}/{}", project_id, version_id, &file_name);
let upload_data = file_host
.upload_file(content_type, &file_path, data.freeze())
.upload_file(content_type, &file_path, data)
.await?;
uploaded_files.push(UploadedFile {
@ -777,7 +858,7 @@ pub async fn upload_file(
));
}
version_files.push(models::version_item::VersionFileBuilder {
version_files.push(VersionFileBuilder {
filename: file_name.to_string(),
url: format!("{}/{}", cdn_url, file_path_encode),
hashes: vec![
@ -794,10 +875,7 @@ pub async fn upload_file(
hash: sha512_bytes,
},
],
primary: (validation_result.is_passed()
&& version_files.iter().all(|x| !x.primary)
&& !ignore_primary)
|| force_primary,
primary,
size: upload_data.content_length,
});

View File

@ -79,6 +79,7 @@ where
payout_wallet_type: result.payout_wallet_type,
payout_address: result.payout_address,
}),
has_flame_anvil_key: Some(result.flame_anvil_key.is_some()),
}),
None => Err(AuthenticationError::InvalidCredentials),
}