Public discord webhook (#492)
This commit is contained in:
parent
e96d23cc3f
commit
e809f77461
1
.env
1
.env
@ -9,6 +9,7 @@ LABRINTH_ADMIN_KEY=feedbeef
|
|||||||
RATE_LIMIT_IGNORE_KEY=feedbeef
|
RATE_LIMIT_IGNORE_KEY=feedbeef
|
||||||
|
|
||||||
MODERATION_DISCORD_WEBHOOK=
|
MODERATION_DISCORD_WEBHOOK=
|
||||||
|
PUBLIC_DISCORD_WEBHOOK=
|
||||||
CLOUDFLARE_INTEGRATION=false
|
CLOUDFLARE_INTEGRATION=false
|
||||||
|
|
||||||
DATABASE_URL=postgresql://labrinth:labrinth@localhost/labrinth
|
DATABASE_URL=postgresql://labrinth:labrinth@localhost/labrinth
|
||||||
|
|||||||
@ -66,7 +66,7 @@ log = "0.4.17"
|
|||||||
env_logger = "0.9.1"
|
env_logger = "0.9.1"
|
||||||
thiserror = "1.0.37"
|
thiserror = "1.0.37"
|
||||||
|
|
||||||
sqlx = { version = "0.6.2", features = ["runtime-actix-rustls", "postgres", "chrono", "offline", "macros", "migrate", "decimal"] }
|
sqlx = { version = "0.6.2", features = ["runtime-actix-rustls", "postgres", "chrono", "offline", "macros", "migrate", "decimal", "json"] }
|
||||||
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 = "0.28.0"
|
||||||
|
|||||||
9
migrations/20221206221021_webhook-sent.sql
Normal file
9
migrations/20221206221021_webhook-sent.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE mods ADD COLUMN webhook_sent BOOL NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
UPDATE mods
|
||||||
|
SET webhook_sent = (status = 'approved');
|
||||||
|
|
||||||
|
UPDATE mods
|
||||||
|
SET status = 'withheld'
|
||||||
|
WHERE status = 'unlisted';
|
||||||
1758
sqlx-data.json
1758
sqlx-data.json
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@ use super::DatabaseError;
|
|||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
pub struct ProjectType {
|
pub struct ProjectType {
|
||||||
pub id: ProjectTypeId,
|
pub id: ProjectTypeId,
|
||||||
@ -16,12 +17,13 @@ pub struct Loader {
|
|||||||
pub supported_project_types: Vec<String>,
|
pub supported_project_types: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Deserialize, Debug)]
|
||||||
pub struct GameVersion {
|
pub struct GameVersion {
|
||||||
pub id: GameVersionId,
|
pub id: GameVersionId,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub version_type: String,
|
#[serde(rename = "type")]
|
||||||
pub date: DateTime<Utc>,
|
pub type_: String,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
pub major: bool,
|
pub major: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,8 +509,8 @@ impl GameVersion {
|
|||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
||||||
id: GameVersionId(c.id),
|
id: GameVersionId(c.id),
|
||||||
version: c.version_,
|
version: c.version_,
|
||||||
version_type: c.type_,
|
type_: c.type_,
|
||||||
date: c.created,
|
created: c.created,
|
||||||
major: c.major
|
major: c.major
|
||||||
})) })
|
})) })
|
||||||
.try_collect::<Vec<GameVersion>>()
|
.try_collect::<Vec<GameVersion>>()
|
||||||
@ -542,8 +544,8 @@ impl GameVersion {
|
|||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
||||||
id: GameVersionId(c.id),
|
id: GameVersionId(c.id),
|
||||||
version: c.version_,
|
version: c.version_,
|
||||||
version_type: c.type_,
|
type_: c.type_,
|
||||||
date: c.created,
|
created: c.created,
|
||||||
major: c.major,
|
major: c.major,
|
||||||
})) })
|
})) })
|
||||||
.try_collect::<Vec<GameVersion>>()
|
.try_collect::<Vec<GameVersion>>()
|
||||||
@ -561,8 +563,8 @@ impl GameVersion {
|
|||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
||||||
id: GameVersionId(c.id),
|
id: GameVersionId(c.id),
|
||||||
version: c.version_,
|
version: c.version_,
|
||||||
version_type: c.type_,
|
type_: c.type_,
|
||||||
date: c.created,
|
created: c.created,
|
||||||
major: c.major,
|
major: c.major,
|
||||||
})) })
|
})) })
|
||||||
.try_collect::<Vec<GameVersion>>()
|
.try_collect::<Vec<GameVersion>>()
|
||||||
@ -581,8 +583,8 @@ impl GameVersion {
|
|||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
||||||
id: GameVersionId(c.id),
|
id: GameVersionId(c.id),
|
||||||
version: c.version_,
|
version: c.version_,
|
||||||
version_type: c.type_,
|
type_: c.type_,
|
||||||
date: c.created,
|
created: c.created,
|
||||||
major: c.major,
|
major: c.major,
|
||||||
})) })
|
})) })
|
||||||
.try_collect::<Vec<GameVersion>>()
|
.try_collect::<Vec<GameVersion>>()
|
||||||
|
|||||||
@ -2,6 +2,7 @@ use super::DatabaseError;
|
|||||||
use crate::models::ids::base62_impl::to_base62;
|
use crate::models::ids::base62_impl::to_base62;
|
||||||
use crate::models::ids::random_base62_rng;
|
use crate::models::ids::random_base62_rng;
|
||||||
use censor::Censor;
|
use censor::Censor;
|
||||||
|
use serde::Deserialize;
|
||||||
use sqlx::sqlx_macros::Type;
|
use sqlx::sqlx_macros::Type;
|
||||||
|
|
||||||
const ID_RETRY_COUNT: usize = 20;
|
const ID_RETRY_COUNT: usize = 20;
|
||||||
@ -136,7 +137,7 @@ pub struct DonationPlatformId(pub i32);
|
|||||||
#[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Hash)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
pub struct VersionId(pub i64);
|
pub struct VersionId(pub i64);
|
||||||
#[derive(Copy, Clone, Debug, Type)]
|
#[derive(Copy, Clone, Debug, Type, Deserialize)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
pub struct GameVersionId(pub i32);
|
pub struct GameVersionId(pub i32);
|
||||||
#[derive(Copy, Clone, Debug, Type)]
|
#[derive(Copy, Clone, Debug, Type)]
|
||||||
|
|||||||
@ -134,6 +134,7 @@ impl ProjectBuilder {
|
|||||||
moderation_message_body: None,
|
moderation_message_body: None,
|
||||||
flame_anvil_project: None,
|
flame_anvil_project: None,
|
||||||
flame_anvil_user: None,
|
flame_anvil_user: None,
|
||||||
|
webhook_sent: false,
|
||||||
};
|
};
|
||||||
project_struct.insert(&mut *transaction).await?;
|
project_struct.insert(&mut *transaction).await?;
|
||||||
|
|
||||||
@ -211,6 +212,7 @@ pub struct Project {
|
|||||||
pub moderation_message_body: Option<String>,
|
pub moderation_message_body: Option<String>,
|
||||||
pub flame_anvil_project: Option<i32>,
|
pub flame_anvil_project: Option<i32>,
|
||||||
pub flame_anvil_user: Option<UserId>,
|
pub flame_anvil_user: Option<UserId>,
|
||||||
|
pub webhook_sent: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
@ -277,7 +279,7 @@ impl Project {
|
|||||||
issues_url, source_url, wiki_url, discord_url, license_url,
|
issues_url, source_url, wiki_url, discord_url, license_url,
|
||||||
team_id, client_side, server_side, license, slug,
|
team_id, client_side, server_side, license, slug,
|
||||||
moderation_message, moderation_message_body, flame_anvil_project,
|
moderation_message, moderation_message_body, flame_anvil_project,
|
||||||
flame_anvil_user
|
flame_anvil_user, webhook_sent
|
||||||
FROM mods
|
FROM mods
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
",
|
",
|
||||||
@ -318,6 +320,7 @@ impl Project {
|
|||||||
approved: row.approved,
|
approved: row.approved,
|
||||||
flame_anvil_project: row.flame_anvil_project,
|
flame_anvil_project: row.flame_anvil_project,
|
||||||
flame_anvil_user: row.flame_anvil_user.map(UserId),
|
flame_anvil_user: row.flame_anvil_user.map(UserId),
|
||||||
|
webhook_sent: row.webhook_sent,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@ -343,7 +346,7 @@ impl Project {
|
|||||||
issues_url, source_url, wiki_url, discord_url, license_url,
|
issues_url, source_url, wiki_url, discord_url, license_url,
|
||||||
team_id, client_side, server_side, license, slug,
|
team_id, client_side, server_side, license, slug,
|
||||||
moderation_message, moderation_message_body, flame_anvil_project,
|
moderation_message, moderation_message_body, flame_anvil_project,
|
||||||
flame_anvil_user
|
flame_anvil_user, webhook_sent
|
||||||
FROM mods
|
FROM mods
|
||||||
WHERE id = ANY($1)
|
WHERE id = ANY($1)
|
||||||
",
|
",
|
||||||
@ -384,6 +387,7 @@ impl Project {
|
|||||||
approved: m.approved,
|
approved: m.approved,
|
||||||
flame_anvil_project: m.flame_anvil_project,
|
flame_anvil_project: m.flame_anvil_project,
|
||||||
flame_anvil_user: m.flame_anvil_user.map(UserId),
|
flame_anvil_user: m.flame_anvil_user.map(UserId),
|
||||||
|
webhook_sent: m.webhook_sent,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.try_collect::<Vec<Project>>()
|
.try_collect::<Vec<Project>>()
|
||||||
@ -662,7 +666,7 @@ impl Project {
|
|||||||
m.updated updated, m.approved approved, m.status status, m.requested_status requested_status,
|
m.updated updated, m.approved approved, m.status status, m.requested_status requested_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.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,
|
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,
|
||||||
cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user,
|
cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent webhook_sent,
|
||||||
ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories,
|
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 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,
|
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,
|
||||||
@ -736,6 +740,7 @@ impl Project {
|
|||||||
approved: m.approved,
|
approved: m.approved,
|
||||||
flame_anvil_project: m.flame_anvil_project,
|
flame_anvil_project: m.flame_anvil_project,
|
||||||
flame_anvil_user: m.flame_anvil_user.map(UserId),
|
flame_anvil_user: m.flame_anvil_user.map(UserId),
|
||||||
|
webhook_sent: m.webhook_sent,
|
||||||
},
|
},
|
||||||
project_type: m.project_type_name,
|
project_type: m.project_type_name,
|
||||||
categories,
|
categories,
|
||||||
@ -848,7 +853,7 @@ impl Project {
|
|||||||
m.updated updated, m.approved approved, m.status status, m.requested_status requested_status,
|
m.updated updated, m.approved approved, m.status status, m.requested_status requested_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.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,
|
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,
|
||||||
cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user,
|
cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent,
|
||||||
ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories,
|
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 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,
|
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,
|
||||||
@ -925,7 +930,8 @@ impl Project {
|
|||||||
moderation_message_body: m.moderation_message_body,
|
moderation_message_body: m.moderation_message_body,
|
||||||
approved: m.approved,
|
approved: m.approved,
|
||||||
flame_anvil_project: m.flame_anvil_project,
|
flame_anvil_project: m.flame_anvil_project,
|
||||||
flame_anvil_user: m.flame_anvil_user.map(UserId)
|
flame_anvil_user: m.flame_anvil_user.map(UserId),
|
||||||
|
webhook_sent: m.webhook_sent,
|
||||||
},
|
},
|
||||||
project_type: m.project_type_name,
|
project_type: m.project_type_name,
|
||||||
categories,
|
categories,
|
||||||
|
|||||||
@ -493,6 +493,7 @@ impl From<QueryVersion> for Version {
|
|||||||
|
|
||||||
/// A status decides the visibility of a project in search, URLs, and the whole site itself.
|
/// A status decides the visibility of a project in search, URLs, and the whole site itself.
|
||||||
/// Listed - Version is displayed on project, and accessible by URL
|
/// Listed - Version is displayed on project, and accessible by URL
|
||||||
|
/// Archived - Identical to listed but has a message displayed stating version is unsupported
|
||||||
/// Draft - Version is not displayed on project, and not accessible by URL
|
/// Draft - Version is not displayed on project, and not accessible by URL
|
||||||
/// Unlisted - Version is not displayed on project, and accessible by URL
|
/// Unlisted - Version is not displayed on project, and accessible by URL
|
||||||
/// Scheduled - Version is scheduled to be released in the future
|
/// Scheduled - Version is scheduled to be released in the future
|
||||||
@ -500,6 +501,7 @@ impl From<QueryVersion> for Version {
|
|||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum VersionStatus {
|
pub enum VersionStatus {
|
||||||
Listed,
|
Listed,
|
||||||
|
Archived,
|
||||||
Draft,
|
Draft,
|
||||||
Unlisted,
|
Unlisted,
|
||||||
Scheduled,
|
Scheduled,
|
||||||
@ -518,12 +520,14 @@ impl VersionStatus {
|
|||||||
"listed" => VersionStatus::Listed,
|
"listed" => VersionStatus::Listed,
|
||||||
"draft" => VersionStatus::Draft,
|
"draft" => VersionStatus::Draft,
|
||||||
"unlisted" => VersionStatus::Unlisted,
|
"unlisted" => VersionStatus::Unlisted,
|
||||||
|
"scheduled" => VersionStatus::Scheduled,
|
||||||
_ => VersionStatus::Unknown,
|
_ => VersionStatus::Unknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn as_str(&self) -> &'static str {
|
pub fn as_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
VersionStatus::Listed => "listed",
|
VersionStatus::Listed => "listed",
|
||||||
|
VersionStatus::Archived => "archived",
|
||||||
VersionStatus::Draft => "draft",
|
VersionStatus::Draft => "draft",
|
||||||
VersionStatus::Unlisted => "unlisted",
|
VersionStatus::Unlisted => "unlisted",
|
||||||
VersionStatus::Unknown => "unknown",
|
VersionStatus::Unknown => "unknown",
|
||||||
@ -534,6 +538,7 @@ impl VersionStatus {
|
|||||||
pub fn iterator() -> impl Iterator<Item = VersionStatus> {
|
pub fn iterator() -> impl Iterator<Item = VersionStatus> {
|
||||||
[
|
[
|
||||||
VersionStatus::Listed,
|
VersionStatus::Listed,
|
||||||
|
VersionStatus::Archived,
|
||||||
VersionStatus::Draft,
|
VersionStatus::Draft,
|
||||||
VersionStatus::Unlisted,
|
VersionStatus::Unlisted,
|
||||||
VersionStatus::Scheduled,
|
VersionStatus::Scheduled,
|
||||||
@ -547,6 +552,7 @@ impl VersionStatus {
|
|||||||
pub fn is_hidden(&self) -> bool {
|
pub fn is_hidden(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
VersionStatus::Listed => false,
|
VersionStatus::Listed => false,
|
||||||
|
VersionStatus::Archived => false,
|
||||||
VersionStatus::Unlisted => false,
|
VersionStatus::Unlisted => false,
|
||||||
|
|
||||||
VersionStatus::Draft => true,
|
VersionStatus::Draft => true,
|
||||||
@ -557,13 +563,14 @@ impl VersionStatus {
|
|||||||
|
|
||||||
// Whether version is listed on project / returned in aggregate routes
|
// Whether version is listed on project / returned in aggregate routes
|
||||||
pub fn is_listed(&self) -> bool {
|
pub fn is_listed(&self) -> bool {
|
||||||
matches!(self, VersionStatus::Listed)
|
matches!(self, VersionStatus::Listed | VersionStatus::Archived)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether a version status can be requested
|
// Whether a version status can be requested
|
||||||
pub fn can_be_requested(&self) -> bool {
|
pub fn can_be_requested(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
VersionStatus::Listed => true,
|
VersionStatus::Listed => true,
|
||||||
|
VersionStatus::Archived => true,
|
||||||
VersionStatus::Draft => true,
|
VersionStatus::Draft => true,
|
||||||
VersionStatus::Unlisted => true,
|
VersionStatus::Unlisted => true,
|
||||||
VersionStatus::Scheduled => false,
|
VersionStatus::Scheduled => false,
|
||||||
|
|||||||
@ -226,6 +226,8 @@ pub enum ApiError {
|
|||||||
Crypto(String),
|
Crypto(String),
|
||||||
#[error("Payments Error: {0}")]
|
#[error("Payments Error: {0}")]
|
||||||
Payments(String),
|
Payments(String),
|
||||||
|
#[error("Discord Error: {0}")]
|
||||||
|
DiscordError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl actix_web::ResponseError for ApiError {
|
impl actix_web::ResponseError for ApiError {
|
||||||
@ -272,6 +274,9 @@ impl actix_web::ResponseError for ApiError {
|
|||||||
ApiError::Payments(..) => {
|
ApiError::Payments(..) => {
|
||||||
actix_web::http::StatusCode::FAILED_DEPENDENCY
|
actix_web::http::StatusCode::FAILED_DEPENDENCY
|
||||||
}
|
}
|
||||||
|
ApiError::DiscordError(..) => {
|
||||||
|
actix_web::http::StatusCode::FAILED_DEPENDENCY
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,6 +299,7 @@ impl actix_web::ResponseError for ApiError {
|
|||||||
ApiError::Analytics(..) => "analytics_error",
|
ApiError::Analytics(..) => "analytics_error",
|
||||||
ApiError::Crypto(..) => "crypto_error",
|
ApiError::Crypto(..) => "crypto_error",
|
||||||
ApiError::Payments(..) => "payments_error",
|
ApiError::Payments(..) => "payments_error",
|
||||||
|
ApiError::DiscordError(..) => "discord_error",
|
||||||
},
|
},
|
||||||
description: &self.to_string(),
|
description: &self.to_string(),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -277,6 +277,7 @@ pub async fn project_create(
|
|||||||
&***file_host,
|
&***file_host,
|
||||||
&flame_anvil_queue,
|
&flame_anvil_queue,
|
||||||
&mut uploaded_files,
|
&mut uploaded_files,
|
||||||
|
&*client,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@ -334,6 +335,7 @@ pub async fn project_create_inner(
|
|||||||
file_host: &dyn FileHost,
|
file_host: &dyn FileHost,
|
||||||
flame_anvil_queue: &Mutex<FlameAnvilQueue>,
|
flame_anvil_queue: &Mutex<FlameAnvilQueue>,
|
||||||
uploaded_files: &mut Vec<UploadedFile>,
|
uploaded_files: &mut Vec<UploadedFile>,
|
||||||
|
pool: &PgPool,
|
||||||
) -> Result<HttpResponse, CreateError> {
|
) -> Result<HttpResponse, CreateError> {
|
||||||
// The base URL for files uploaded to backblaze
|
// The base URL for files uploaded to backblaze
|
||||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||||
@ -817,7 +819,8 @@ pub async fn project_create_inner(
|
|||||||
if let Ok(webhook_url) = dotenvy::var("MODERATION_DISCORD_WEBHOOK")
|
if let Ok(webhook_url) = dotenvy::var("MODERATION_DISCORD_WEBHOOK")
|
||||||
{
|
{
|
||||||
crate::util::webhook::send_discord_webhook(
|
crate::util::webhook::send_discord_webhook(
|
||||||
response.clone(),
|
response.id,
|
||||||
|
pool,
|
||||||
webhook_url,
|
webhook_url,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@ -499,7 +499,8 @@ pub async fn project_edit(
|
|||||||
dotenvy::var("MODERATION_DISCORD_WEBHOOK")
|
dotenvy::var("MODERATION_DISCORD_WEBHOOK")
|
||||||
{
|
{
|
||||||
crate::util::webhook::send_discord_webhook(
|
crate::util::webhook::send_discord_webhook(
|
||||||
Project::from(project_item.clone()),
|
project_item.inner.id.into(),
|
||||||
|
&*pool,
|
||||||
webhook_url,
|
webhook_url,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@ -507,7 +508,9 @@ pub async fn project_edit(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.is_approved() {
|
if status.is_approved()
|
||||||
|
&& !project_item.inner.status.is_approved()
|
||||||
|
{
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE mods
|
UPDATE mods
|
||||||
@ -520,6 +523,31 @@ pub async fn project_edit(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
webhook_url,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE mods
|
||||||
|
SET webhook_sent = TRUE
|
||||||
|
WHERE id = $1
|
||||||
|
",
|
||||||
|
id as database::models::ids::ProjectId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE mods
|
UPDATE mods
|
||||||
|
|||||||
@ -231,8 +231,8 @@ pub async fn game_version_list(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| GameVersionQueryData {
|
.map(|x| GameVersionQueryData {
|
||||||
version: x.version,
|
version: x.version,
|
||||||
version_type: x.version_type,
|
version_type: x.type_,
|
||||||
date: x.date,
|
date: x.created,
|
||||||
major: x.major,
|
major: x.major,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@ -133,6 +133,7 @@ pub async fn mod_create(
|
|||||||
&***file_host,
|
&***file_host,
|
||||||
&flame_anvil_queue,
|
&flame_anvil_queue,
|
||||||
&mut uploaded_files,
|
&mut uploaded_files,
|
||||||
|
&*client,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,10 @@ use std::sync::Arc;
|
|||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
|
fn default_requested_status() -> VersionStatus {
|
||||||
|
VersionStatus::Listed
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Validate, Clone)]
|
#[derive(Serialize, Deserialize, Validate, Clone)]
|
||||||
pub struct InitialVersionData {
|
pub struct InitialVersionData {
|
||||||
#[serde(alias = "mod_id")]
|
#[serde(alias = "mod_id")]
|
||||||
@ -59,6 +63,7 @@ pub struct InitialVersionData {
|
|||||||
pub loaders: Vec<Loader>,
|
pub loaders: Vec<Loader>,
|
||||||
pub featured: bool,
|
pub featured: bool,
|
||||||
pub primary_file: Option<String>,
|
pub primary_file: Option<String>,
|
||||||
|
#[serde(default = "default_requested_status")]
|
||||||
pub status: VersionStatus,
|
pub status: VersionStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,30 @@
|
|||||||
use crate::models::projects::Project;
|
use crate::database::models::categories::GameVersion;
|
||||||
|
use crate::models::projects::ProjectId;
|
||||||
|
use crate::routes::ApiError;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use std::usize;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct DiscordEmbed {
|
struct DiscordEmbed {
|
||||||
|
pub author: Option<DiscordEmbedAuthor>,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub timestamp: DateTime<Utc>,
|
pub timestamp: DateTime<Utc>,
|
||||||
pub color: u32,
|
pub color: u32,
|
||||||
pub fields: Vec<DiscordEmbedField>,
|
pub fields: Vec<DiscordEmbedField>,
|
||||||
pub image: DiscordEmbedImage,
|
pub thumbnail: DiscordEmbedThumbnail,
|
||||||
|
pub image: Option<DiscordEmbedImage>,
|
||||||
|
pub footer: Option<DiscordEmbedFooter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct DiscordEmbedAuthor {
|
||||||
|
pub name: String,
|
||||||
|
pub url: Option<String>,
|
||||||
|
pub icon_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@ -25,72 +39,182 @@ struct DiscordEmbedImage {
|
|||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct DiscordEmbedThumbnail {
|
||||||
|
pub url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct DiscordEmbedFooter {
|
||||||
|
pub text: String,
|
||||||
|
pub icon_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct DiscordWebhook {
|
struct DiscordWebhook {
|
||||||
|
pub avatar_url: Option<String>,
|
||||||
|
pub username: Option<String>,
|
||||||
pub embeds: Vec<DiscordEmbed>,
|
pub embeds: Vec<DiscordEmbed>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_discord_webhook(
|
pub async fn send_discord_webhook(
|
||||||
project: Project,
|
project_id: ProjectId,
|
||||||
|
pool: &PgPool,
|
||||||
webhook_url: String,
|
webhook_url: String,
|
||||||
) -> Result<(), reqwest::Error> {
|
) -> Result<(), ApiError> {
|
||||||
let mut fields = vec![
|
let row =
|
||||||
DiscordEmbedField {
|
sqlx::query!(
|
||||||
name: "id",
|
"
|
||||||
value: project.id.to_string(),
|
SELECT m.id id, m.title title, m.description description,
|
||||||
inline: true,
|
m.icon_url icon_url, m.slug slug, cs.name client_side_type, ss.name server_side_type,
|
||||||
},
|
pt.name project_type, u.username username, u.avatar_url avatar_url,
|
||||||
DiscordEmbedField {
|
ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories,
|
||||||
name: "project_type",
|
ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,
|
||||||
value: project.project_type.clone(),
|
JSONB_AGG(DISTINCT TO_JSONB(gv)) filter (where gv.version is not null) versions,
|
||||||
inline: true,
|
JSONB_AGG(DISTINCT TO_JSONB(agv)) filter (where gv.version is not null) all_game_versions,
|
||||||
},
|
ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null) gallery
|
||||||
DiscordEmbedField {
|
FROM mods m
|
||||||
name: "client_side",
|
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id AND mc.is_additional = FALSE
|
||||||
value: project.client_side.to_string(),
|
LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id
|
||||||
inline: true,
|
LEFT OUTER JOIN versions v ON v.mod_id = m.id AND v.status != ANY($2)
|
||||||
},
|
LEFT OUTER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id
|
||||||
DiscordEmbedField {
|
LEFT OUTER JOIN game_versions gv ON gvv.game_version_id = gv.id
|
||||||
name: "server_side",
|
LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id
|
||||||
value: project.server_side.to_string(),
|
LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id
|
||||||
inline: true,
|
LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id
|
||||||
},
|
LEFT OUTER JOIN game_versions agv ON 1=1
|
||||||
];
|
INNER JOIN project_types pt ON pt.id = m.project_type
|
||||||
|
INNER JOIN side_types cs ON m.client_side = cs.id
|
||||||
|
INNER JOIN side_types ss ON m.server_side = ss.id
|
||||||
|
INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE
|
||||||
|
INNER JOIN users u ON tm.user_id = u.id
|
||||||
|
WHERE m.id = $1
|
||||||
|
GROUP BY m.id, cs.id, ss.id, pt.id, u.id;
|
||||||
|
",
|
||||||
|
project_id.0 as i64,
|
||||||
|
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
|
||||||
|
crate::models::teams::OWNER_ROLE,
|
||||||
|
)
|
||||||
|
.fetch_optional(&*pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
if !project.categories.is_empty() {
|
if let Some(project) = row {
|
||||||
|
let mut fields = vec![];
|
||||||
|
|
||||||
|
let categories = project.categories.unwrap_or_default();
|
||||||
|
let loaders = project.loaders.unwrap_or_default();
|
||||||
|
|
||||||
|
let versions: Vec<GameVersion> =
|
||||||
|
serde_json::from_value(project.versions.unwrap_or_default())
|
||||||
|
.map_err(|err| {
|
||||||
|
ApiError::DiscordError(
|
||||||
|
"Error while sending projects webhook".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let all_game_versions: Vec<GameVersion> = serde_json::from_value(
|
||||||
|
project.all_game_versions.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
.map_err(|err| {
|
||||||
|
ApiError::DiscordError(
|
||||||
|
"Error while sending projects webhook".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !categories.is_empty() {
|
||||||
fields.push(DiscordEmbedField {
|
fields.push(DiscordEmbedField {
|
||||||
name: "categories",
|
name: "Categories",
|
||||||
value: project.categories.join(", "),
|
value: categories
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut x| format!("{}{x}", x.remove(0).to_uppercase()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n"),
|
||||||
inline: true,
|
inline: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref slug) = project.slug {
|
if !loaders.is_empty() {
|
||||||
|
let mut formatted_loaders: String = String::new();
|
||||||
|
|
||||||
|
for loader in loaders {
|
||||||
|
let emoji_id: i64 = match &*loader {
|
||||||
|
"bukkit" => 1049793345481883689,
|
||||||
|
"bungeecord" => 1049793347067314220,
|
||||||
|
"fabric" => 1049793348719890532,
|
||||||
|
"forge" => 1049793350498275358,
|
||||||
|
"liteloader" => 1049793351630733333,
|
||||||
|
"minecraft" => 1049793352964526100,
|
||||||
|
"modloader" => 1049793353962762382,
|
||||||
|
"paper" => 1049793355598540810,
|
||||||
|
"purpur" => 1049793357351751772,
|
||||||
|
"quilt" => 1049793857681887342,
|
||||||
|
"rift" => 1049793359373414502,
|
||||||
|
"spigot" => 1049793413886779413,
|
||||||
|
"sponge" => 1049793416969605231,
|
||||||
|
"velocity" => 1049793419108700170,
|
||||||
|
"waterfall" => 1049793420937412638,
|
||||||
|
_ => 1049805243866681424,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut x = loader.clone();
|
||||||
|
formatted_loaders.push_str(&format!(
|
||||||
|
"<:{loader}:{emoji_id}> {}{x}\n",
|
||||||
|
x.remove(0).to_uppercase()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
fields.push(DiscordEmbedField {
|
fields.push(DiscordEmbedField {
|
||||||
name: "slug",
|
name: "Loaders",
|
||||||
value: slug.clone(),
|
value: formatted_loaders,
|
||||||
|
inline: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !versions.is_empty() {
|
||||||
|
let mut formatted_game_versions: String =
|
||||||
|
get_gv_range(versions, all_game_versions);
|
||||||
|
|
||||||
|
fields.push(DiscordEmbedField {
|
||||||
|
name: "Versions",
|
||||||
|
value: formatted_game_versions,
|
||||||
inline: true,
|
inline: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let embed = DiscordEmbed {
|
let embed = DiscordEmbed {
|
||||||
|
author: Some(DiscordEmbedAuthor {
|
||||||
|
name: project.username.clone(),
|
||||||
|
url: Some(format!(
|
||||||
|
"{}/user/{}",
|
||||||
|
dotenvy::var("SITE_URL").unwrap_or_default(),
|
||||||
|
project.username
|
||||||
|
)),
|
||||||
|
icon_url: project.avatar_url,
|
||||||
|
}),
|
||||||
url: format!(
|
url: format!(
|
||||||
"{}/{}/{}",
|
"{}/{}/{}",
|
||||||
dotenvy::var("SITE_URL").unwrap_or_default(),
|
dotenvy::var("SITE_URL").unwrap_or_default(),
|
||||||
project.project_type,
|
project.project_type,
|
||||||
project
|
project.slug.unwrap_or_else(|| project_id.to_string())
|
||||||
.clone()
|
|
||||||
.slug
|
|
||||||
.unwrap_or_else(|| project.id.to_string())
|
|
||||||
),
|
),
|
||||||
title: project.title,
|
title: project.title,
|
||||||
description: project.description,
|
description: project.description,
|
||||||
timestamp: project.published,
|
timestamp: Utc::now(),
|
||||||
color: 0x1bd96a,
|
color: 0x1bd96a,
|
||||||
fields,
|
fields,
|
||||||
image: DiscordEmbedImage {
|
thumbnail: DiscordEmbedThumbnail {
|
||||||
url: project.icon_url,
|
url: project.icon_url,
|
||||||
},
|
},
|
||||||
|
image: project.gallery.unwrap_or_default().first().map(|x| {
|
||||||
|
DiscordEmbedImage {
|
||||||
|
url: Some(x.to_string()),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
footer: Some(DiscordEmbedFooter {
|
||||||
|
text: "Modrinth".to_string(),
|
||||||
|
icon_url: Some(
|
||||||
|
"https://cdn-raw.modrinth.com/modrinth-new.png".to_string(),
|
||||||
|
),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
@ -98,10 +222,161 @@ pub async fn send_discord_webhook(
|
|||||||
client
|
client
|
||||||
.post(&webhook_url)
|
.post(&webhook_url)
|
||||||
.json(&DiscordWebhook {
|
.json(&DiscordWebhook {
|
||||||
|
avatar_url: Some(
|
||||||
|
"https://cdn.modrinth.com/Modrinth_Dark_Logo.png"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
username: Some("Modrinth Release".to_string()),
|
||||||
embeds: vec![embed],
|
embeds: vec![embed],
|
||||||
})
|
})
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
ApiError::DiscordError(
|
||||||
|
"Error while sending projects webhook".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_gv_range(
|
||||||
|
mut game_versions: Vec<GameVersion>,
|
||||||
|
mut all_game_versions: Vec<GameVersion>,
|
||||||
|
) -> String {
|
||||||
|
// both -> least to greatest
|
||||||
|
game_versions.sort_by(|a, b| a.created.cmp(&b.created));
|
||||||
|
|
||||||
|
all_game_versions.sort_by(|a, b| a.created.cmp(&b.created));
|
||||||
|
|
||||||
|
let all_releases = all_game_versions
|
||||||
|
.iter()
|
||||||
|
.filter(|x| &*x.type_ == "release")
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut intervals = Vec::new();
|
||||||
|
let mut current_interval = 0;
|
||||||
|
|
||||||
|
const MAX_VALUE: usize = 1000000;
|
||||||
|
|
||||||
|
for i in 0..game_versions.len() {
|
||||||
|
let current_version = &*game_versions[i].version;
|
||||||
|
|
||||||
|
let index = all_game_versions
|
||||||
|
.iter()
|
||||||
|
.position(|x| &*x.version == current_version)
|
||||||
|
.unwrap_or(MAX_VALUE);
|
||||||
|
let release_index = all_releases
|
||||||
|
.iter()
|
||||||
|
.position(|x| &*x.version == current_version)
|
||||||
|
.unwrap_or(MAX_VALUE);
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
intervals.push(vec![vec![i, index, release_index]])
|
||||||
|
} else {
|
||||||
|
let interval_base = &intervals[current_interval];
|
||||||
|
|
||||||
|
if ((index as i32)
|
||||||
|
- (interval_base[interval_base.len() - 1][1] as i32)
|
||||||
|
== 1
|
||||||
|
|| (release_index as i32)
|
||||||
|
- (interval_base[interval_base.len() - 1][2] as i32)
|
||||||
|
== 1)
|
||||||
|
&& (all_game_versions[interval_base[0][1]].type_ == "release"
|
||||||
|
|| all_game_versions[index].type_ != "release")
|
||||||
|
{
|
||||||
|
if intervals[current_interval].get(1).is_some() {
|
||||||
|
intervals[current_interval][1] =
|
||||||
|
vec![i, index, release_index];
|
||||||
|
} else {
|
||||||
|
intervals[current_interval]
|
||||||
|
.insert(1, vec![i, index, release_index]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current_interval += 1;
|
||||||
|
intervals.push(vec![vec![i, index, release_index]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_intervals = Vec::new();
|
||||||
|
|
||||||
|
for interval in intervals {
|
||||||
|
if interval.len() == 2
|
||||||
|
&& interval[0][2] != MAX_VALUE
|
||||||
|
&& interval[1][2] == MAX_VALUE
|
||||||
|
{
|
||||||
|
let mut last_snapshot: Option<usize> = None;
|
||||||
|
|
||||||
|
for j in ((interval[0][1] + 1)..=interval[1][1]).rev() {
|
||||||
|
if all_game_versions[j].type_ == "release" {
|
||||||
|
new_intervals.push(vec![
|
||||||
|
interval[0].clone(),
|
||||||
|
vec![
|
||||||
|
game_versions
|
||||||
|
.iter()
|
||||||
|
.position(|x| {
|
||||||
|
x.version == all_game_versions[j].version
|
||||||
|
})
|
||||||
|
.unwrap_or(MAX_VALUE),
|
||||||
|
j,
|
||||||
|
all_releases
|
||||||
|
.iter()
|
||||||
|
.position(|x| {
|
||||||
|
x.version == all_game_versions[j].version
|
||||||
|
})
|
||||||
|
.unwrap_or(MAX_VALUE),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if let Some(last_snapshot) = last_snapshot {
|
||||||
|
if last_snapshot != j + 1 {
|
||||||
|
new_intervals.push(vec![
|
||||||
|
vec![
|
||||||
|
game_versions
|
||||||
|
.iter()
|
||||||
|
.position(|x| {
|
||||||
|
x.version
|
||||||
|
== all_game_versions
|
||||||
|
[last_snapshot]
|
||||||
|
.version
|
||||||
|
})
|
||||||
|
.unwrap_or(MAX_VALUE),
|
||||||
|
last_snapshot,
|
||||||
|
MAX_VALUE,
|
||||||
|
],
|
||||||
|
interval[1].clone(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
new_intervals.push(vec![interval[1].clone()])
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
last_snapshot = Some(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
new_intervals.push(interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
|
for interval in new_intervals {
|
||||||
|
if interval.len() == 2 {
|
||||||
|
output.push(format!(
|
||||||
|
"{}—{}",
|
||||||
|
&game_versions[interval[0][0]].version,
|
||||||
|
&game_versions[interval[1][0]].version
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
output.push(game_versions[interval[0][0]].version.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.join("\n")
|
||||||
|
}
|
||||||
|
|||||||
@ -151,7 +151,7 @@ fn game_version_supported(
|
|||||||
all_game_versions
|
all_game_versions
|
||||||
.iter()
|
.iter()
|
||||||
.find(|y| y.version == x.0)
|
.find(|y| y.version == x.0)
|
||||||
.map(|x| x.date > date)
|
.map(|x| x.created > date)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -160,7 +160,7 @@ fn game_version_supported(
|
|||||||
all_game_versions
|
all_game_versions
|
||||||
.iter()
|
.iter()
|
||||||
.find(|y| y.version == x.0)
|
.find(|y| y.version == x.0)
|
||||||
.map(|x| x.date > before && x.date < after)
|
.map(|x| x.created > before && x.created < after)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user