Skip to content
Open
Show file tree
Hide file tree
Changes from all 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_BILLION: f64 = 1_000_000_000.0;

pub fn abbreviate_number(n: i32) -> String {
let n = n as f64;
if n.abs() >= ONE_BILLION {
format!("{:.1}B", n / ONE_BILLION)
} 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)
}
}
6 changes: 3 additions & 3 deletions src/database/repository/mod_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ impl ModVersionRow {
geode: self.geode,
early_load: self.early_load,
requires_patching: self.requires_patching,
download_count: self.download_count,
download_count: self.download_count.into(),
api: self.api,
mod_id: self.mod_id,
status: self.status,
Expand Down Expand Up @@ -224,7 +224,7 @@ pub async fn create_from_json(
download_link: row.download_link,
hash: row.hash,
geode: geode.to_string(),
download_count: 0,
download_count: 0.into(),
early_load: row.early_load,
requires_patching: row.requires_patching,
api: row.api,
Expand Down Expand Up @@ -333,7 +333,7 @@ pub async fn update_pending_version(
download_link: row.download_link,
hash: row.hash,
geode: geode.to_string(),
download_count: row.download_count,
download_count: row.download_count.into(),
early_load: row.early_load,
requires_patching: row.requires_patching,
api: row.api,
Expand Down
2 changes: 1 addition & 1 deletion src/database/repository/mods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ impl ModRecordGetOne {
id: self.id,
repository: self.repository,
featured: self.featured,
download_count: self.download_count,
download_count: self.download_count.into(),
versions: Default::default(),
tags: Default::default(),
developers: Default::default(),
Expand Down
1 change: 1 addition & 0 deletions src/endpoints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod developers;
pub mod health;
pub mod loader;
pub mod mod_versions;
pub mod mod_status_badge;
pub mod mods;
pub mod stats;
pub mod tags;
Expand Down
88 changes: 88 additions & 0 deletions src/endpoints/mod_status_badge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use crate::config::AppData;
use crate::endpoints::ApiError;
use actix_web::{HttpResponse, Responder, get, web};
use serde::Deserialize;
use utoipa::{IntoParams, ToSchema};

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

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

#[derive(Deserialize, Clone, Copy, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum StatusBadgeStat {
Version,
GdVersion,
GeodeVersion,
Downloads,
}

#[derive(Deserialize, IntoParams)]
pub struct StatusBadgeQuery {
pub stat: StatusBadgeStat,
}

#[utoipa::path(
get,
path = "/v1/mods/{id}/status_badge",
tag = "mods",
params(
("id" = String, Path, description = "Mod ID"),
StatusBadgeQuery
),
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 {
StatusBadgeStat::Version => (
"payload.versions[0].version",
"Version",
"static/mod_version.svg",
),
StatusBadgeStat::GdVersion => (
"payload.versions[0].gd.win",
"Geometry Dash",
"static/mod_gd_version.svg",
),
StatusBadgeStat::GeodeVersion => (
"payload.versions[0].geode",
"Geode",
"static/mod_geode_version.svg",
),
StatusBadgeStat::Downloads => (
"payload.download_count",
"Downloads",
"static/mod_downloads.svg",
),
};
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", data.app_url(), id);
let mod_link = format!("{}/mods/{}", data.front_url(), id);
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())
}
7 changes: 7 additions & 0 deletions src/endpoints/mods.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#[derive(Deserialize, ToSchema)]
pub struct ModGetQueryParams {
pub abbreviate: Option<bool>,
}
use crate::config::AppData;
use crate::database::repository::developers;
use crate::database::repository::incompatibilities;
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,6 +176,8 @@ pub async fn get(
i.modify_metadata(data.app_url(), has_extended_permissions);
}

the_mod.set_abbreviated_download_counts(query.abbreviate.unwrap_or(false));

Ok(web::Json(ApiResponse {
error: "".into(),
payload: the_mod,
Expand Down
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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 +65,7 @@ async fn main() -> anyhow::Result<()> {
.service(endpoints::mods::create)
.service(endpoints::mods::update_mod)
.service(endpoints::mods::get_logo)
.service(endpoints::mod_status_badge::status_badge)
.service(endpoints::mod_versions::get_version_index)
.service(endpoints::mod_versions::get_one)
.service(endpoints::mod_versions::download_version)
Expand Down
40 changes: 40 additions & 0 deletions src/types/models/download_count.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use crate::abbreviate::abbreviate_number;
use serde::{Serialize, Serializer};

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DownloadCount {
count: i32,
abbreviate: bool,
}

impl DownloadCount {
pub const fn new(count: i32) -> Self {
Self {
count,
abbreviate: false,
}
}

pub fn set_abbreviated(&mut self, abbreviate: bool) {
self.abbreviate = abbreviate;
}
}

impl From<i32> for DownloadCount {
fn from(count: i32) -> Self {
Self::new(count)
}
}

impl Serialize for DownloadCount {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if self.abbreviate {
serializer.serialize_str(&abbreviate_number(self.count))
} else {
serializer.serialize_i32(self.count)
}
}
}
1 change: 1 addition & 0 deletions src/types/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod dependency;
pub mod download_count;
pub mod developer;
pub mod github_login_attempt;
pub mod incompatibility;
Expand Down
19 changes: 14 additions & 5 deletions src/types/models/mod_entity.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::{
dependency::ResponseDependency,
download_count::DownloadCount,
developer::ModDeveloper,
incompatibility::{Replacement, ResponseIncompatibility},
mod_gd_version::{DetailedGDVersion, GDVersionEnum, ModGDVersion, VerPlatform},
Expand Down Expand Up @@ -35,7 +36,8 @@ pub struct Mod {
pub id: String,
pub repository: Option<String>,
pub featured: bool,
pub download_count: i32,
#[schema(value_type = i32)]
pub download_count: DownloadCount,
pub developers: Vec<ModDeveloper>,
pub versions: Vec<ModVersion>,
pub tags: Vec<String>,
Expand Down Expand Up @@ -107,6 +109,13 @@ pub struct ModStats {
}

impl Mod {
pub fn set_abbreviated_download_counts(&mut self, abbreviate: bool) {
self.download_count.set_abbreviated(abbreviate);
for version in &mut self.versions {
version.set_abbreviated_download_count(abbreviate);
}
}

pub async fn get_stats(pool: &mut PgConnection) -> Result<ModStats, DatabaseError> {
let result = sqlx::query!(
"
Expand Down Expand Up @@ -356,7 +365,7 @@ impl Mod {
Mod {
id: x.id,
repository: x.repository,
download_count: x.download_count,
download_count: x.download_count.into(),
featured: x.featured,
versions: vec![version],
tags,
Expand Down Expand Up @@ -403,7 +412,7 @@ impl Mod {
Mod {
id: x.id.clone(),
repository: x.repository.clone(),
download_count: x.download_count,
download_count: x.download_count.into(),
featured: x.featured,
versions: version,
tags,
Expand Down Expand Up @@ -548,7 +557,7 @@ impl Mod {
description: x.description.clone(),
version: x.version.clone(),
download_link: x.download_link.clone(),
download_count: x.mod_version_download_count,
download_count: x.mod_version_download_count.into(),
hash: x.hash.clone(),
geode: x.geode.clone(),
early_load: x.early_load,
Expand Down Expand Up @@ -592,7 +601,7 @@ impl Mod {
id: records[0].id.clone(),
repository: records[0].repository.clone(),
featured: records[0].featured,
download_count: records[0].mod_download_count,
download_count: records[0].mod_download_count.into(),
versions,
tags,
developers: devs,
Expand Down
10 changes: 8 additions & 2 deletions src/types/models/mod_version.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::{
dependency::{Dependency, ModVersionCompare, ResponseDependency},
download_count::DownloadCount,
developer::ModDeveloper,
incompatibility::{Incompatibility, ResponseIncompatibility},
mod_gd_version::{DetailedGDVersion, GDVersionEnum, ModGDVersion, VerPlatform},
Expand Down Expand Up @@ -31,7 +32,8 @@ pub struct ModVersion {
pub download_link: String,
pub hash: String,
pub geode: String,
pub download_count: i32,
#[schema(value_type = i32)]
pub download_count: DownloadCount,
pub early_load: bool,
pub requires_patching: bool,
pub api: bool,
Expand Down Expand Up @@ -101,7 +103,7 @@ impl ModVersionGetOne {
geode: self.geode.clone(),
early_load: self.early_load,
requires_patching: self.requires_patching,
download_count: self.download_count,
download_count: self.download_count.into(),
api: self.api,
mod_id: self.mod_id.clone(),
status: self.status,
Expand All @@ -128,6 +130,10 @@ impl ModVersionGetOne {
}

impl ModVersion {
pub fn set_abbreviated_download_count(&mut self, abbreviate: bool) {
self.download_count.set_abbreviated(abbreviate);
}

fn modify_download_link(&mut self, app_url: &str) {
self.download_link = create_download_link(app_url, &self.mod_id, &self.version)
}
Expand Down
1 change: 1 addition & 0 deletions static/mod_downloads.svg
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to serve these statically from the web application?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I have a plan to add app storage (which would have to be served statically by the reverse proxy) in #60, though that PR is WIP.

I could backport the storage bit to main.

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.