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
4 changes: 4 additions & 0 deletions migrations/20260208100437_new_deprecations.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Add down migration script here

DROP TABLE deprecated_by;
DROP TABLE deprecations;
15 changes: 15 additions & 0 deletions migrations/20260208100437_new_deprecations.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- Add up migration script here

CREATE TABLE deprecations (
id SERIAL PRIMARY KEY NOT NULL,
mod_id TEXT NOT NULL,
reason TEXT NOT NULL,
FOREIGN KEY (mod_id) REFERENCES mods(id) ON DELETE CASCADE
);
CREATE TABLE deprecated_by (
deprecation_id INTEGER NOT NULL,
by_mod_id TEXT NOT NULL,
-- If we want to add mod-specific reasons, add them here
FOREIGN KEY (deprecation_id) REFERENCES deprecations(id) ON DELETE CASCADE,
FOREIGN KEY (by_mod_id) REFERENCES mods(id)
);
78 changes: 78 additions & 0 deletions src/endpoints/deprecations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@

use actix_web::{HttpResponse, Responder, get, post, web};
use serde::Deserialize;
use crate::{
config::AppData,
database::repository::{developers, mods},
endpoints::ApiError,
extractors::auth::Auth,
types::{api::ApiResponse, models::deprecations::Deprecation}
};

#[derive(Deserialize)]
struct DeprecationPath {
id: String,
}

#[derive(Deserialize)]
struct UpdateDeprecationParams {
by: Vec<String>,
reason: String,
}

/// Update the deprecation status of a mod
#[get("v1/mods/{id}/deprecate")]
pub async fn check_mod_deprecation(
data: web::Data<AppData>,
path: web::Path<DeprecationPath>,
auth: Auth,
) -> Result<impl Responder, ApiError> {
let dev = auth.developer()?;
let mut pool = data.db().acquire().await?;

if !mods::exists(&path.id, &mut pool).await? {
return Err(ApiError::NotFound(format!("Mod id {} not found", path.id)));
}
// Allow admins to deprecate any mod
if !dev.admin && !developers::owns_mod(dev.id, &path.id, &mut pool).await? {
return Err(ApiError::Authorization);
}

let deps = Deprecation::get_deprecations_for(&mut pool, std::slice::from_ref(&path.id)).await?;
Ok(web::Json(ApiResponse {
error: "".into(),
payload: deps.into_iter().next()
}))
}

/// Update the deprecation status of a mod
#[post("v1/mods/{id}/deprecate")]
pub async fn deprecate_mod(
data: web::Data<AppData>,
path: web::Path<DeprecationPath>,
json: web::Json<Option<UpdateDeprecationParams>>,
auth: Auth,
) -> Result<impl Responder, ApiError> {
let dev = auth.developer()?;
let mut pool = data.db().acquire().await?;

if !mods::exists(&path.id, &mut pool).await? {
return Err(ApiError::NotFound(format!("Mod id {} not found", path.id)));
}
// Allow admins to deprecate any mod
if !dev.admin && !developers::owns_mod(dev.id, &path.id, &mut pool).await? {
return Err(ApiError::Authorization);
}

// Delete old deprecation data (if it exists)
// If we're updating deprecations, just replace the data
Deprecation::delete_deprecation(&mut pool, &path.id).await?;

// Add new deprecation data if provided
if let Some(json) = json.0 {
Deprecation::create_deprecation(&mut pool, &path.id, &json.by, &json.reason).await?;
}

Ok(HttpResponse::NoContent())
}

1 change: 1 addition & 0 deletions src/endpoints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod mod_versions;
pub mod mods;
pub mod stats;
pub mod tags;
pub mod deprecations;

#[derive(thiserror::Error, Debug)]
pub enum ApiError {
Expand Down
54 changes: 25 additions & 29 deletions src/endpoints/mods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::mod_zip;
use crate::types::api::{create_download_link, ApiResponse};
use crate::types::mod_json::ModJson;
use crate::types::models;
use crate::types::models::deprecations::Deprecation;
use crate::types::models::incompatibility::Incompatibility;
use crate::types::models::mod_entity::{Mod, ModUpdate};
use crate::types::models::mod_gd_version::{GDVersionEnum, VerPlatform};
Expand All @@ -22,6 +23,7 @@ use crate::types::models::mod_version_status::ModVersionStatusEnum;
use crate::webhook::discord::DiscordWebhook;
use actix_web::{get, post, put, web, HttpResponse, Responder};
use serde::Deserialize;
use serde::Serialize;
use sqlx::Acquire;

#[derive(Deserialize, Default)]
Expand Down Expand Up @@ -234,6 +236,16 @@ struct UpdateQueryParams {
platform: VerPlatform,
geode: String,
}
#[derive(Serialize)]
#[serde(untagged)]
enum UpdateQueryResponse {
V4(Vec<ModUpdate>),
V5 {
updates: Vec<ModUpdate>,
deprecations: Vec<Deprecation>,
}
}

#[get("/v1/mods/updates")]
pub async fn get_mod_updates(
data: web::Data<AppData>,
Expand All @@ -258,40 +270,24 @@ pub async fn get_mod_updates(
))
})?;

let mut result: Vec<ModUpdate> =
let result: Vec<ModUpdate> =
Mod::get_updates(&ids, query.platform, &geode, query.gd, &mut pool).await?;
let mut replacements =
Incompatibility::get_supersedes_for(&ids, query.platform, query.gd, &geode, &mut pool)
.await?;

for i in &mut result {
if let Some(replacement) = replacements.get(&i.id) {
let mut clone = replacement.clone();
clone.download_link = create_download_link(data.app_url(), &clone.id, &clone.version);
i.replacement = Some(clone);
replacements.remove_entry(&i.id);
}
i.download_link = create_download_link(data.app_url(), &i.id, &i.version);
}

for i in replacements {
let mut replacement = i.1.clone();
replacement.download_link =
create_download_link(data.app_url(), &replacement.id, &replacement.version);
result.push(ModUpdate {
id: i.0.clone(),
version: "1.0.0".to_string(),
mod_version_id: 0,
download_link: replacement.download_link.clone(),
replacement: Some(replacement),
dependencies: vec![],
incompatibilities: vec![],
});

// On v5, we return deprecations as a separate array
if geode.major >= 5 {
let deprecations = Deprecation::get_deprecations_for(&mut pool, &ids).await?;
return Ok(web::Json(ApiResponse {
error: "".into(),
payload: UpdateQueryResponse::V5 {
updates: result,
deprecations,
}
}));
}

Ok(web::Json(ApiResponse {
error: "".into(),
payload: result,
payload: UpdateQueryResponse::V4(result),
}))
}

Expand Down
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ async fn main() -> anyhow::Result<()> {
.service(endpoints::mod_versions::download_version)
.service(endpoints::mod_versions::create_version)
.service(endpoints::mod_versions::update_version)
.service(endpoints::deprecations::check_mod_deprecation)
.service(endpoints::deprecations::deprecate_mod)
.service(endpoints::auth::github::start_github_web_login)
.service(endpoints::auth::refresh_token)
.service(endpoints::auth::github::github_web_callback)
Expand Down
83 changes: 83 additions & 0 deletions src/types/models/deprecations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use serde::Serialize;
use sqlx::PgConnection;

use crate::endpoints::ApiError;

#[derive(sqlx::FromRow, Serialize)]
pub struct Deprecation {
pub mod_id: String,
pub by: Vec<String>,
pub reason: String,
}

impl Deprecation {
pub async fn get_deprecations_for(pool: &mut PgConnection, ids: &[String])
-> Result<Vec<Deprecation>, ApiError>
{
let deps = sqlx::query!(
r#"
SELECT d.id, d.mod_id, d.reason
FROM deprecations d
WHERE d.mod_id = ANY($1)
"#,
ids
).fetch_all(&mut *pool).await?;

let mut bys: Vec<_> = sqlx::query!(
r#"
SELECT dby.deprecation_id, dby.by_mod_id
FROM deprecated_by dby
WHERE dby.deprecation_id = ANY($1)
"#,
&deps.iter().map(|d| d.id).collect::<Vec<i32>>()
).fetch_all(&mut *pool).await?;

Ok(deps.into_iter().map(|dep| Deprecation {
mod_id: dep.mod_id,
by: bys
.extract_if(.., |by| by.deprecation_id == dep.id)
.map(|by| by.by_mod_id)
.collect(),
reason: dep.reason,
}).collect())
}

pub async fn create_deprecation(pool: &mut PgConnection, mod_id: &str, by: &[String], reason: &str)
-> Result<(), ApiError>
{
let id = sqlx::query!(
r#"
INSERT INTO deprecations(mod_id, reason)
VALUES ($1, $2)
RETURNING id
"#,
mod_id, reason
).fetch_one(&mut *pool).await?.id;

// I'm not sure how you'd insert multiple values at once with sqlx but
// this endpoint shouldn't be called very often so this oughta be fine
for by_id in by {
sqlx::query!(
r#"
INSERT INTO deprecated_by(deprecation_id, by_mod_id)
VALUES ($1, $2)
"#,
id, by_id
).execute(&mut *pool).await?;
}

Ok(())
}
pub async fn delete_deprecation(pool: &mut PgConnection, mod_id: &str)
-> Result<(), ApiError>
{
sqlx::query!(
r#"
DELETE FROM deprecations
WHERE mod_id = $1
"#,
mod_id
).execute(&mut *pool).await?;
Ok(())
}
}
Loading
Loading