You've already forked AstralRinth
forked from didirus/AstralRinth
FlameAnvil Project Sync (#481)
* FlameAnvil Project Sync * Perm fixes * Fix compile * Fix clippy + run prepare
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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>>()
|
||||
|
||||
13
src/main.rs
13
src/main.rs
@@ -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)
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
250
src/queue/flameanvil.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod download;
|
||||
pub mod flameanvil;
|
||||
pub mod payouts;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(""))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user