You've already forked AstralRinth
forked from didirus/AstralRinth
Fix clippy errors + lint, use turbo CI
This commit is contained in:
@@ -20,11 +20,17 @@ pub enum MultipartSegmentData {
|
||||
}
|
||||
|
||||
pub trait AppendsMultipart {
|
||||
fn set_multipart(self, data: impl IntoIterator<Item = MultipartSegment>) -> Self;
|
||||
fn set_multipart(
|
||||
self,
|
||||
data: impl IntoIterator<Item = MultipartSegment>,
|
||||
) -> Self;
|
||||
}
|
||||
|
||||
impl AppendsMultipart for TestRequest {
|
||||
fn set_multipart(self, data: impl IntoIterator<Item = MultipartSegment>) -> Self {
|
||||
fn set_multipart(
|
||||
self,
|
||||
data: impl IntoIterator<Item = MultipartSegment>,
|
||||
) -> Self {
|
||||
let (boundary, payload) = generate_multipart(data);
|
||||
self.append_header((
|
||||
"Content-Type",
|
||||
@@ -34,7 +40,9 @@ impl AppendsMultipart for TestRequest {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_multipart(data: impl IntoIterator<Item = MultipartSegment>) -> (String, Bytes) {
|
||||
pub fn generate_multipart(
|
||||
data: impl IntoIterator<Item = MultipartSegment>,
|
||||
) -> (String, Bytes) {
|
||||
let mut boundary: String = String::from("----WebKitFormBoundary");
|
||||
boundary.push_str(&rand::random::<u64>().to_string());
|
||||
boundary.push_str(&rand::random::<u64>().to_string());
|
||||
@@ -54,7 +62,8 @@ pub fn generate_multipart(data: impl IntoIterator<Item = MultipartSegment>) -> (
|
||||
|
||||
if let Some(filename) = &segment.filename {
|
||||
payload.extend_from_slice(
|
||||
format!("; filename=\"{filename}\"", filename = filename).as_bytes(),
|
||||
format!("; filename=\"{filename}\"", filename = filename)
|
||||
.as_bytes(),
|
||||
);
|
||||
}
|
||||
if let Some(content_type) = &segment.content_type {
|
||||
@@ -78,7 +87,9 @@ pub fn generate_multipart(data: impl IntoIterator<Item = MultipartSegment>) -> (
|
||||
}
|
||||
payload.extend_from_slice(b"\r\n");
|
||||
}
|
||||
payload.extend_from_slice(format!("--{boundary}--\r\n", boundary = boundary).as_bytes());
|
||||
payload.extend_from_slice(
|
||||
format!("--{boundary}--\r\n", boundary = boundary).as_bytes(),
|
||||
);
|
||||
|
||||
(boundary, Bytes::from(payload))
|
||||
}
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
macro_rules! bitflags_serde_impl {
|
||||
($type:ident, $int_type:ident) => {
|
||||
impl serde::Serialize for $type {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
fn serialize<S: serde::Serializer>(
|
||||
&self,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_i64(self.bits() as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for $type {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Self, D::Error> {
|
||||
let v: i64 = Deserialize::deserialize(deserializer)?;
|
||||
|
||||
Ok($type::from_bits_truncate(v as $int_type))
|
||||
|
||||
@@ -4,7 +4,10 @@ use actix_web::HttpRequest;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
pub async fn check_turnstile_captcha(req: &HttpRequest, challenge: &str) -> Result<bool, ApiError> {
|
||||
pub async fn check_turnstile_captcha(
|
||||
req: &HttpRequest,
|
||||
challenge: &str,
|
||||
) -> Result<bool, ApiError> {
|
||||
let conn_info = req.connection_info().clone();
|
||||
let ip_addr = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) {
|
||||
if let Some(header) = req.headers().get("CF-Connecting-IP") {
|
||||
|
||||
@@ -2,8 +2,9 @@ use actix_web::guard::GuardContext;
|
||||
|
||||
pub const ADMIN_KEY_HEADER: &str = "Modrinth-Admin";
|
||||
pub fn admin_key_guard(ctx: &GuardContext) -> bool {
|
||||
let admin_key = std::env::var("LABRINTH_ADMIN_KEY")
|
||||
.expect("No admin key provided, this should have been caught by check_env_vars");
|
||||
let admin_key = std::env::var("LABRINTH_ADMIN_KEY").expect(
|
||||
"No admin key provided, this should have been caught by check_env_vars",
|
||||
);
|
||||
ctx.head()
|
||||
.headers()
|
||||
.get(ADMIN_KEY_HEADER)
|
||||
|
||||
@@ -6,7 +6,10 @@ use crate::models::images::ImageContext;
|
||||
use crate::routes::ApiError;
|
||||
use color_thief::ColorFormat;
|
||||
use image::imageops::FilterType;
|
||||
use image::{DynamicImage, EncodableLayout, GenericImageView, ImageError, ImageOutputFormat};
|
||||
use image::{
|
||||
DynamicImage, EncodableLayout, GenericImageView, ImageError,
|
||||
ImageOutputFormat,
|
||||
};
|
||||
use std::io::Cursor;
|
||||
use webp::Encoder;
|
||||
|
||||
@@ -14,10 +17,15 @@ pub fn get_color_from_img(data: &[u8]) -> Result<Option<u32>, ImageError> {
|
||||
let image = image::load_from_memory(data)?
|
||||
.resize(256, 256, FilterType::Nearest)
|
||||
.crop_imm(128, 128, 64, 64);
|
||||
let color = color_thief::get_palette(image.to_rgb8().as_bytes(), ColorFormat::Rgb, 10, 2)
|
||||
.ok()
|
||||
.and_then(|x| x.first().copied())
|
||||
.map(|x| (x.r as u32) << 16 | (x.g as u32) << 8 | (x.b as u32));
|
||||
let color = color_thief::get_palette(
|
||||
image.to_rgb8().as_bytes(),
|
||||
ColorFormat::Rgb,
|
||||
10,
|
||||
2,
|
||||
)
|
||||
.ok()
|
||||
.and_then(|x| x.first().copied())
|
||||
.map(|x| (x.r as u32) << 16 | (x.g as u32) << 8 | (x.b as u32));
|
||||
|
||||
Ok(color)
|
||||
}
|
||||
@@ -40,16 +48,23 @@ pub async fn upload_image_optimized(
|
||||
min_aspect_ratio: Option<f32>,
|
||||
file_host: &dyn FileHost,
|
||||
) -> Result<UploadImageResult, ApiError> {
|
||||
let content_type =
|
||||
crate::util::ext::get_image_content_type(file_extension).ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!("Invalid format for image: {}", file_extension))
|
||||
let content_type = crate::util::ext::get_image_content_type(file_extension)
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Invalid format for image: {}",
|
||||
file_extension
|
||||
))
|
||||
})?;
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let (processed_image, processed_image_ext) =
|
||||
process_image(bytes.clone(), content_type, target_width, min_aspect_ratio)?;
|
||||
let (processed_image, processed_image_ext) = process_image(
|
||||
bytes.clone(),
|
||||
content_type,
|
||||
target_width,
|
||||
min_aspect_ratio,
|
||||
)?;
|
||||
let color = get_color_from_img(&bytes)?;
|
||||
|
||||
// Only upload the processed image if it's smaller than the original
|
||||
@@ -118,7 +133,8 @@ fn process_image(
|
||||
|
||||
if let Some(target_width) = target_width {
|
||||
if img.width() > target_width {
|
||||
let new_height = (target_width as f32 / aspect_ratio).round() as u32;
|
||||
let new_height =
|
||||
(target_width as f32 / aspect_ratio).round() as u32;
|
||||
img = img.resize(target_width, new_height, FilterType::Lanczos3);
|
||||
}
|
||||
}
|
||||
@@ -126,7 +142,8 @@ fn process_image(
|
||||
if let Some(min_aspect_ratio) = min_aspect_ratio {
|
||||
// Crop if necessary
|
||||
if aspect_ratio < min_aspect_ratio {
|
||||
let crop_height = (img.width() as f32 / min_aspect_ratio).round() as u32;
|
||||
let crop_height =
|
||||
(img.width() as f32 / min_aspect_ratio).round() as u32;
|
||||
let y_offset = (img.height() - crop_height) / 2;
|
||||
img = img.crop_imm(0, y_offset, img.width(), crop_height);
|
||||
}
|
||||
@@ -181,7 +198,9 @@ pub async fn delete_unused_images(
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), ApiError> {
|
||||
let uploaded_images = database::models::Image::get_many_contexted(context, transaction).await?;
|
||||
let uploaded_images =
|
||||
database::models::Image::get_many_contexted(context, transaction)
|
||||
.await?;
|
||||
|
||||
for image in uploaded_images {
|
||||
let mut should_delete = true;
|
||||
|
||||
@@ -13,8 +13,12 @@ use actix_web::{
|
||||
use futures_util::future::LocalBoxFuture;
|
||||
use futures_util::future::{ready, Ready};
|
||||
|
||||
pub type KeyedRateLimiter<K = String, MW = middleware::StateInformationMiddleware> =
|
||||
Arc<RateLimiter<K, state::keyed::DefaultKeyedStateStore<K>, DefaultClock, MW>>;
|
||||
pub type KeyedRateLimiter<
|
||||
K = String,
|
||||
MW = middleware::StateInformationMiddleware,
|
||||
> = Arc<
|
||||
RateLimiter<K, state::keyed::DefaultKeyedStateStore<K>, DefaultClock, MW>,
|
||||
>;
|
||||
|
||||
pub struct RateLimit(pub KeyedRateLimiter);
|
||||
|
||||
@@ -58,7 +62,9 @@ where
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
if let Some(key) = req.headers().get("x-ratelimit-key") {
|
||||
if key.to_str().ok() == dotenvy::var("RATE_LIMIT_IGNORE_KEY").ok().as_deref() {
|
||||
if key.to_str().ok()
|
||||
== dotenvy::var("RATE_LIMIT_IGNORE_KEY").ok().as_deref()
|
||||
{
|
||||
let res = self.service.call(req);
|
||||
|
||||
return Box::pin(async move {
|
||||
@@ -129,7 +135,8 @@ where
|
||||
})
|
||||
}
|
||||
Err(negative) => {
|
||||
let wait_time = negative.wait_time_from(DefaultClock::default().now());
|
||||
let wait_time =
|
||||
negative.wait_time_from(DefaultClock::default().now());
|
||||
|
||||
let mut response = ApiError::RateLimitError(
|
||||
wait_time.as_millis(),
|
||||
@@ -140,28 +147,41 @@ where
|
||||
let headers = response.headers_mut();
|
||||
|
||||
headers.insert(
|
||||
actix_web::http::header::HeaderName::from_str("x-ratelimit-limit").unwrap(),
|
||||
actix_web::http::header::HeaderName::from_str(
|
||||
"x-ratelimit-limit",
|
||||
)
|
||||
.unwrap(),
|
||||
negative.quota().burst_size().get().into(),
|
||||
);
|
||||
headers.insert(
|
||||
actix_web::http::header::HeaderName::from_str("x-ratelimit-remaining")
|
||||
.unwrap(),
|
||||
actix_web::http::header::HeaderName::from_str(
|
||||
"x-ratelimit-remaining",
|
||||
)
|
||||
.unwrap(),
|
||||
0.into(),
|
||||
);
|
||||
headers.insert(
|
||||
actix_web::http::header::HeaderName::from_str("x-ratelimit-reset").unwrap(),
|
||||
actix_web::http::header::HeaderName::from_str(
|
||||
"x-ratelimit-reset",
|
||||
)
|
||||
.unwrap(),
|
||||
wait_time.as_secs().into(),
|
||||
);
|
||||
|
||||
Box::pin(async { Ok(req.into_response(response.map_into_right_body())) })
|
||||
Box::pin(async {
|
||||
Ok(req.into_response(response.map_into_right_body()))
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let response =
|
||||
ApiError::CustomAuthentication("Unable to obtain user IP address!".to_string())
|
||||
.error_response();
|
||||
let response = ApiError::CustomAuthentication(
|
||||
"Unable to obtain user IP address!".to_string(),
|
||||
)
|
||||
.error_response();
|
||||
|
||||
Box::pin(async { Ok(req.into_response(response.map_into_right_body())) })
|
||||
Box::pin(async {
|
||||
Ok(req.into_response(response.map_into_right_body()))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@ pub async fn redis_execute<T>(
|
||||
where
|
||||
T: redis::FromRedisValue,
|
||||
{
|
||||
let res = cmd.query_async::<_, T>(redis).await?;
|
||||
let res = cmd.query_async::<T>(redis).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ pub async fn read_from_payload(
|
||||
return Err(ApiError::InvalidInput(String::from(err_msg)));
|
||||
} else {
|
||||
bytes.extend_from_slice(&item.map_err(|_| {
|
||||
ApiError::InvalidInput("Unable to parse bytes in payload sent!".to_string())
|
||||
ApiError::InvalidInput(
|
||||
"Unable to parse bytes in payload sent!".to_string(),
|
||||
)
|
||||
})?);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,15 @@ use validator::{ValidationErrors, ValidationErrorsKind};
|
||||
use crate::models::pats::Scopes;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref RE_URL_SAFE: Regex = Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap();
|
||||
pub static ref RE_URL_SAFE: Regex =
|
||||
Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap();
|
||||
}
|
||||
|
||||
//TODO: In order to ensure readability, only the first error is printed, this may need to be expanded on in the future!
|
||||
pub fn validation_errors_to_string(errors: ValidationErrors, adder: Option<String>) -> String {
|
||||
pub fn validation_errors_to_string(
|
||||
errors: ValidationErrors,
|
||||
adder: Option<String>,
|
||||
) -> String {
|
||||
let mut output = String::new();
|
||||
|
||||
let map = errors.into_errors();
|
||||
@@ -21,7 +25,10 @@ pub fn validation_errors_to_string(errors: ValidationErrors, adder: Option<Strin
|
||||
if let Some(error) = map.get(field) {
|
||||
return match error {
|
||||
ValidationErrorsKind::Struct(errors) => {
|
||||
validation_errors_to_string(*errors.clone(), Some(format!("of item {field}")))
|
||||
validation_errors_to_string(
|
||||
*errors.clone(),
|
||||
Some(format!("of item {field}")),
|
||||
)
|
||||
}
|
||||
ValidationErrorsKind::List(list) => {
|
||||
if let Some((index, errors)) = list.iter().next() {
|
||||
@@ -113,7 +120,9 @@ pub fn validate_url_hashmap_values(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_no_restricted_scopes(value: &Scopes) -> Result<(), validator::ValidationError> {
|
||||
pub fn validate_no_restricted_scopes(
|
||||
value: &Scopes,
|
||||
) -> Result<(), validator::ValidationError> {
|
||||
if value.is_restricted() {
|
||||
return Err(validator::ValidationError::new(
|
||||
"Restricted scopes not allowed",
|
||||
|
||||
@@ -47,9 +47,12 @@ async fn get_webhook_metadata(
|
||||
redis: &RedisPool,
|
||||
emoji: bool,
|
||||
) -> Result<Option<WebhookMetadata>, ApiError> {
|
||||
let project =
|
||||
crate::database::models::project_item::Project::get_id(project_id.into(), pool, redis)
|
||||
.await?;
|
||||
let project = crate::database::models::project_item::Project::get_id(
|
||||
project_id.into(),
|
||||
pool,
|
||||
redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(mut project) = project {
|
||||
let mut owner = None;
|
||||
@@ -82,9 +85,12 @@ async fn get_webhook_metadata(
|
||||
.await?;
|
||||
|
||||
if let Some(member) = team.into_iter().find(|x| x.is_owner) {
|
||||
let user =
|
||||
crate::database::models::user_item::User::get_id(member.user_id, pool, redis)
|
||||
.await?;
|
||||
let user = crate::database::models::user_item::User::get_id(
|
||||
member.user_id,
|
||||
pool,
|
||||
redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(user) = user {
|
||||
owner = Some(WebhookAuthor {
|
||||
@@ -100,13 +106,16 @@ async fn get_webhook_metadata(
|
||||
}
|
||||
};
|
||||
|
||||
let all_game_versions = MinecraftGameVersion::list(None, None, pool, redis).await?;
|
||||
let all_game_versions =
|
||||
MinecraftGameVersion::list(None, None, pool, redis).await?;
|
||||
|
||||
let versions = project
|
||||
.aggregate_version_fields
|
||||
.clone()
|
||||
.into_iter()
|
||||
.find_map(|vf| MinecraftGameVersion::try_from_version_field(&vf).ok())
|
||||
.find_map(|vf| {
|
||||
MinecraftGameVersion::try_from_version_field(&vf).ok()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let formatted_game_versions = get_gv_range(versions, all_game_versions);
|
||||
@@ -196,7 +205,10 @@ async fn get_webhook_metadata(
|
||||
_ => 1049805243866681424,
|
||||
};
|
||||
|
||||
format!("<:{loader}:{emoji_id}> {}{x}", x.remove(0).to_uppercase())
|
||||
format!(
|
||||
"<:{loader}:{emoji_id}> {}{x}",
|
||||
x.remove(0).to_uppercase()
|
||||
)
|
||||
} else {
|
||||
format!("{}{x}", x.remove(0).to_uppercase())
|
||||
}
|
||||
@@ -323,7 +335,11 @@ pub async fn send_slack_webhook(
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| ApiError::Discord("Error while sending projects webhook".to_string()))?;
|
||||
.map_err(|_| {
|
||||
ApiError::Discord(
|
||||
"Error while sending projects webhook".to_string(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -436,7 +452,9 @@ pub async fn send_discord_webhook(
|
||||
.map(|x| DiscordEmbedImage { url: Some(x) }),
|
||||
footer: Some(DiscordEmbedFooter {
|
||||
text: format!("{} on Modrinth", project.display_project_type),
|
||||
icon_url: Some("https://cdn-raw.modrinth.com/modrinth-new.png".to_string()),
|
||||
icon_url: Some(
|
||||
"https://cdn-raw.modrinth.com/modrinth-new.png".to_string(),
|
||||
),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -445,14 +463,21 @@ pub async fn send_discord_webhook(
|
||||
client
|
||||
.post(&webhook_url)
|
||||
.json(&DiscordWebhook {
|
||||
avatar_url: Some("https://cdn.modrinth.com/Modrinth_Dark_Logo.png".to_string()),
|
||||
avatar_url: Some(
|
||||
"https://cdn.modrinth.com/Modrinth_Dark_Logo.png"
|
||||
.to_string(),
|
||||
),
|
||||
username: Some("Modrinth Release".to_string()),
|
||||
embeds: vec![embed],
|
||||
content: message,
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| ApiError::Discord("Error while sending projects webhook".to_string()))?;
|
||||
.map_err(|_| {
|
||||
ApiError::Discord(
|
||||
"Error while sending projects webhook".to_string(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -496,15 +521,21 @@ fn get_gv_range(
|
||||
} 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)
|
||||
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];
|
||||
intervals[current_interval][1] =
|
||||
vec![i, index, release_index];
|
||||
} else {
|
||||
intervals[current_interval].insert(1, vec![i, index, release_index]);
|
||||
intervals[current_interval]
|
||||
.insert(1, vec![i, index, release_index]);
|
||||
}
|
||||
} else {
|
||||
current_interval += 1;
|
||||
@@ -516,7 +547,10 @@ fn get_gv_range(
|
||||
let mut new_intervals = Vec::new();
|
||||
|
||||
for interval in intervals {
|
||||
if interval.len() == 2 && interval[0][2] != MAX_VALUE && interval[1][2] == MAX_VALUE {
|
||||
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() {
|
||||
@@ -526,12 +560,16 @@ fn get_gv_range(
|
||||
vec![
|
||||
game_versions
|
||||
.iter()
|
||||
.position(|x| x.version == all_game_versions[j].version)
|
||||
.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)
|
||||
.position(|x| {
|
||||
x.version == all_game_versions[j].version
|
||||
})
|
||||
.unwrap_or(MAX_VALUE),
|
||||
],
|
||||
]);
|
||||
@@ -543,7 +581,10 @@ fn get_gv_range(
|
||||
game_versions
|
||||
.iter()
|
||||
.position(|x| {
|
||||
x.version == all_game_versions[last_snapshot].version
|
||||
x.version
|
||||
== all_game_versions
|
||||
[last_snapshot]
|
||||
.version
|
||||
})
|
||||
.unwrap_or(MAX_VALUE),
|
||||
last_snapshot,
|
||||
@@ -572,7 +613,8 @@ fn get_gv_range(
|
||||
if interval.len() == 2 {
|
||||
output.push(format!(
|
||||
"{}—{}",
|
||||
&game_versions[interval[0][0]].version, &game_versions[interval[1][0]].version
|
||||
&game_versions[interval[0][0]].version,
|
||||
&game_versions[interval[1][0]].version
|
||||
))
|
||||
} else {
|
||||
output.push(game_versions[interval[0][0]].version.clone())
|
||||
|
||||
Reference in New Issue
Block a user