Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ thiserror = "2.0.12"
moka = { version = "0.12.13", features = ["future"] }
utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "uuid"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["actix-web"] }
urlencoding = "2.1.3"
16 changes: 16 additions & 0 deletions src/abbreviate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const ONE_THOUSAND: f64 = 1_000.0;
const ONE_MILLION: f64 = 1_000_000.0;
const ONE_MORBILLION: f64 = 1_000_000_000.0;
Comment thread
TreehouseFalcon marked this conversation as resolved.
Outdated

pub fn abbreviate_number(n: i32) -> String {
let n = n as f64;
if n.abs() >= ONE_MORBILLION {
format!("{:.1}B", n / ONE_MORBILLION)
} else if n.abs() >= ONE_MILLION {
format!("{:.1}M", n / ONE_MILLION)
} else if n.abs() >= ONE_THOUSAND {
format!("{:.1}K", n / ONE_THOUSAND)
} else {
format!("{:.0}", n)
}
}
82 changes: 82 additions & 0 deletions src/endpoints/mod_status_badge.rs
Comment thread
TreehouseFalcon marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use crate::config::AppData;
use crate::endpoints::ApiError;
use actix_web::{HttpResponse, Responder, get, web};
use serde::Deserialize;

use std::fs;
use std::path::Path;
use urlencoding;

const LABEL_COLOR: &str = "#0c0811";
const STAT_COLOR: &str = "#5f3d84";

#[derive(Deserialize)]
pub struct StatusBadgeQuery {
pub stat: String,
Comment thread
TreehouseFalcon marked this conversation as resolved.
Outdated
}

#[utoipa::path(
get,
path = "/v1/mods/{id}/status_badge",
tag = "mods",
params(
("id" = String, Path, description = "Mod ID"),
("stat" = String, Query, description = "Stat to display: version, gd_version, geode_version, downloads")
),
responses(
(status = 302, description = "Redirect to Shields.io badge"),
(status = 400, description = "Invalid stat or missing parameter"),
(status = 404, description = "Mod not found")
)
)]
#[get("/v1/mods/{id}/status_badge")]
pub async fn status_badge(
_data: web::Data<AppData>,
id: web::Path<String>,
query: web::Query<StatusBadgeQuery>,
) -> Result<impl Responder, ApiError> {
let (stat, label, svg_path) = match query.stat.as_str() {
"version" => (
"payload.versions[0].version",
"Version",
"static/mod_version.svg",
),
"gd_version" => (
"payload.versions[0].gd.win",
"Geometry Dash",
"static/mod_gd_version.svg",
),
"geode_version" => (
"payload.versions[0].geode",
"Geode",
"static/mod_geode_version.svg",
),
"downloads" => (
"payload.download_count",
"Downloads",
"static/mod_downloads.svg",
),
_ => return Err(ApiError::BadRequest("Invalid stat parameter".into())),
};
let svg = fs::read_to_string(Path::new(svg_path))
.map_err(|_| ApiError::BadRequest(format!("Could not read SVG file: {}", svg_path)))?;
let api_url = format!(
"{}/v1/mods/{}?abbreviate=true",
"http://api.geode-sdk.org", id
Comment thread
TreehouseFalcon marked this conversation as resolved.
Outdated
);
let mod_link = format!("https://geode-sdk.org/mods/{}", id);
Comment thread
TreehouseFalcon marked this conversation as resolved.
Outdated
let svg_data_url = format!("data:image/svg+xml;utf8,{}", urlencoding::encode(&svg));
let shields_url = format!(
"https://img.shields.io/badge/dynamic/json?url={}&query={}&label={}&labelColor={}&color={}&link={}&style=plastic&logo={}",
urlencoding::encode(&api_url),
urlencoding::encode(stat),
label,
urlencoding::encode(LABEL_COLOR),
urlencoding::encode(STAT_COLOR),
urlencoding::encode(&mod_link),
urlencoding::encode(&svg_data_url)
);
Ok(HttpResponse::Found()
.append_header(("Location", shields_url))
.finish())
}
37 changes: 33 additions & 4 deletions src/endpoints/mods.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#[derive(Deserialize, ToSchema)]
pub struct ModGetQueryParams {
pub abbreviate: Option<bool>,
}
use crate::abbreviate::abbreviate_number;
use crate::config::AppData;
use crate::database::repository::developers;
use crate::database::repository::incompatibilities;
Expand All @@ -24,7 +29,7 @@ use actix_web::{HttpResponse, Responder, get, post, put, web};
use serde::Deserialize;
use serde::Serialize;
use sqlx::Acquire;
use utoipa::{ToSchema, IntoParams};
use utoipa::{IntoParams, ToSchema};

#[derive(Deserialize, Default, Hash, Eq, PartialEq, ToSchema)]
#[serde(rename_all = "snake_case")]
Expand Down Expand Up @@ -74,7 +79,6 @@ pub struct CreateQueryParams {
(status = 403, description = "Forbidden")
)
)]

#[get("/v1/mods")]
pub async fn index(
data: web::Data<AppData>,
Expand Down Expand Up @@ -126,6 +130,7 @@ pub async fn index(
pub async fn get(
data: web::Data<AppData>,
id: web::Path<String>,
query: web::Query<ModGetQueryParams>,
auth: Auth,
) -> Result<impl Responder, ApiError> {
let dev = auth.developer().ok();
Expand Down Expand Up @@ -171,9 +176,31 @@ pub async fn get(
i.modify_metadata(data.app_url(), has_extended_permissions);
}

// If abbreviate param is set, abbreviate download_count fields
let mut payload = serde_json::to_value(&the_mod).unwrap();
Comment thread
Fleeym marked this conversation as resolved.
Outdated
if query.abbreviate.unwrap_or(false) {
if let Some(obj) = payload.as_object_mut() {
obj.insert(
"download_count".to_string(),
serde_json::Value::String(abbreviate_number(the_mod.download_count)),
);
if let Some(versions) = obj.get_mut("versions").and_then(|v| v.as_array_mut()) {
for (i, v) in versions.iter_mut().enumerate() {
if let Some(version_obj) = v.as_object_mut() {
if let Some(dc) = the_mod.versions.get(i) {
version_obj.insert(
"download_count".to_string(),
serde_json::Value::String(abbreviate_number(dc.download_count)),
);
}
}
}
}
}
}
Ok(web::Json(ApiResponse {
error: "".into(),
payload: the_mod,
payload,
}))
}

Expand Down Expand Up @@ -208,7 +235,9 @@ pub async fn create(
let existing: Option<Mod> = mods::get_one(&json.id, false, &mut pool).await?;

if json.id.starts_with("geode.") && !dev.admin {
return Err(ApiError::BadRequest("Only index admins may use mod ids that start with 'geode.'".into()));
return Err(ApiError::BadRequest(
"Only index admins may use mod ids that start with 'geode.'".into(),
));
}

if let Some(m) = &existing {
Expand Down
3 changes: 3 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::endpoints::mod_status_badge::status_badge;
use crate::openapi::ApiDoc;
use crate::types::api;
use actix_cors::Cors;
Expand All @@ -9,6 +10,7 @@ use actix_web::{
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;

mod abbreviate;
mod auth;
mod cli;
mod config;
Expand Down Expand Up @@ -64,6 +66,7 @@ async fn main() -> anyhow::Result<()> {
.service(endpoints::mods::create)
.service(endpoints::mods::update_mod)
.service(endpoints::mods::get_logo)
.service(status_badge)
Comment thread
TreehouseFalcon marked this conversation as resolved.
Outdated
.service(endpoints::mod_versions::get_version_index)
.service(endpoints::mod_versions::get_one)
.service(endpoints::mod_versions::download_version)
Expand Down
1 change: 1 addition & 0 deletions static/mod_downloads.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions static/mod_gd_version.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions static/mod_geode_version.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions static/mod_version.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading