diff --git a/src/models/v2/mod.rs b/src/models/v2/mod.rs index 58ea73ba8..ed955b3ad 100644 --- a/src/models/v2/mod.rs +++ b/src/models/v2/mod.rs @@ -1,5 +1,8 @@ // Legacy models from V2, where its useful to keep the struct for rerouting/conversion pub mod notifications; pub mod projects; +pub mod reports; pub mod search; pub mod teams; +pub mod threads; +pub mod user; diff --git a/src/models/v2/reports.rs b/src/models/v2/reports.rs new file mode 100644 index 000000000..4e531326c --- /dev/null +++ b/src/models/v2/reports.rs @@ -0,0 +1,52 @@ +use crate::models::ids::{ReportId, ThreadId, UserId}; +use crate::models::reports::{ItemType, Report}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct LegacyReport { + pub id: ReportId, + pub report_type: String, + pub item_id: String, + pub item_type: LegacyItemType, + pub reporter: UserId, + pub body: String, + pub created: DateTime, + pub closed: bool, + pub thread_id: ThreadId, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub enum LegacyItemType { + Project, + Version, + User, + Unknown, +} +impl From for LegacyItemType { + fn from(x: ItemType) -> Self { + match x { + ItemType::Project => LegacyItemType::Project, + ItemType::Version => LegacyItemType::Version, + ItemType::User => LegacyItemType::User, + ItemType::Unknown => LegacyItemType::Unknown, + } + } +} + +impl From for LegacyReport { + fn from(x: Report) -> Self { + LegacyReport { + id: x.id, + report_type: x.report_type, + item_id: x.item_id, + item_type: x.item_type.into(), + reporter: x.reporter, + body: x.body, + created: x.created, + closed: x.closed, + thread_id: x.thread_id, + } + } +} diff --git a/src/models/v2/threads.rs b/src/models/v2/threads.rs new file mode 100644 index 000000000..dcf9386b0 --- /dev/null +++ b/src/models/v2/threads.rs @@ -0,0 +1,120 @@ +use crate::models::ids::{ImageId, ProjectId, ReportId, ThreadId, ThreadMessageId}; +use crate::models::projects::ProjectStatus; +use crate::models::users::{User, UserId}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct LegacyThread { + pub id: ThreadId, + #[serde(rename = "type")] + pub type_: LegacyThreadType, + pub project_id: Option, + pub report_id: Option, + pub messages: Vec, + pub members: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct LegacyThreadMessage { + pub id: ThreadMessageId, + pub author_id: Option, + pub body: LegacyMessageBody, + pub created: DateTime, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum LegacyMessageBody { + Text { + body: String, + #[serde(default)] + private: bool, + replying_to: Option, + #[serde(default)] + associated_images: Vec, + }, + StatusChange { + new_status: ProjectStatus, + old_status: ProjectStatus, + }, + ThreadClosure, + ThreadReopen, + Deleted, +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)] +#[serde(rename_all = "snake_case")] +pub enum LegacyThreadType { + Report, + Project, + DirectMessage, +} + +impl From for LegacyThreadType { + fn from(t: crate::models::v3::threads::ThreadType) -> Self { + match t { + crate::models::v3::threads::ThreadType::Report => LegacyThreadType::Report, + crate::models::v3::threads::ThreadType::Project => LegacyThreadType::Project, + crate::models::v3::threads::ThreadType::DirectMessage => { + LegacyThreadType::DirectMessage + } + } + } +} + +impl From for LegacyMessageBody { + fn from(b: crate::models::v3::threads::MessageBody) -> Self { + match b { + crate::models::v3::threads::MessageBody::Text { + body, + private, + replying_to, + associated_images, + } => LegacyMessageBody::Text { + body, + private, + replying_to, + associated_images, + }, + crate::models::v3::threads::MessageBody::StatusChange { + new_status, + old_status, + } => LegacyMessageBody::StatusChange { + new_status, + old_status, + }, + crate::models::v3::threads::MessageBody::ThreadClosure => { + LegacyMessageBody::ThreadClosure + } + crate::models::v3::threads::MessageBody::ThreadReopen => { + LegacyMessageBody::ThreadReopen + } + crate::models::v3::threads::MessageBody::Deleted => LegacyMessageBody::Deleted, + } + } +} + +impl From for LegacyThreadMessage { + fn from(m: crate::models::v3::threads::ThreadMessage) -> Self { + LegacyThreadMessage { + id: m.id, + author_id: m.author_id, + body: m.body.into(), + created: m.created, + } + } +} + +impl From for LegacyThread { + fn from(t: crate::models::v3::threads::Thread) -> Self { + LegacyThread { + id: t.id, + type_: t.type_.into(), + project_id: t.project_id, + report_id: t.report_id, + messages: t.messages.into_iter().map(|m| m.into()).collect(), + members: t.members, + } + } +} diff --git a/src/models/v2/user.rs b/src/models/v2/user.rs new file mode 100644 index 000000000..cea8c18da --- /dev/null +++ b/src/models/v2/user.rs @@ -0,0 +1,53 @@ +use crate::{ + auth::AuthProvider, + models::{ + ids::UserId, + users::{Badges, Role, UserPayoutData}, + }, +}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct LegacyUser { + pub id: UserId, + pub username: String, + pub name: Option, + pub avatar_url: Option, + pub bio: Option, + pub created: DateTime, + pub role: Role, + pub badges: Badges, + + pub auth_providers: Option>, // this was changed in v3, but not changes ones we want to keep out of v2 + pub email: Option, + pub email_verified: Option, + pub has_password: Option, + pub has_totp: Option, + pub payout_data: Option, // this was changed in v3, but not ones we want to keep out of v2 + + // DEPRECATED. Always returns None + pub github_id: Option, +} + +impl From for LegacyUser { + fn from(data: crate::models::v3::users::User) -> Self { + Self { + id: data.id, + username: data.username, + name: data.name, + email: data.email, + email_verified: data.email_verified, + avatar_url: data.avatar_url, + bio: data.bio, + created: data.created, + role: data.role, + badges: data.badges, + payout_data: data.payout_data, + auth_providers: data.auth_providers, + has_password: data.has_password, + has_totp: data.has_totp, + github_id: data.github_id, + } + } +} diff --git a/src/routes/v2/moderation.rs b/src/routes/v2/moderation.rs index b9ac981ac..fac02f151 100644 --- a/src/routes/v2/moderation.rs +++ b/src/routes/v2/moderation.rs @@ -1,4 +1,6 @@ use super::ApiError; +use crate::models::projects::Project; +use crate::models::v2::projects::LegacyProject; use crate::queue::session::AuthQueue; use crate::routes::v3; use crate::{database::redis::RedisPool, routes::v2_reroute}; @@ -28,13 +30,22 @@ pub async fn get_projects( count: web::Query, session_queue: web::Data, ) -> Result { - v3::moderation::get_projects( + let response = v3::moderation::get_projects( req, - pool, - redis, + pool.clone(), + redis.clone(), web::Query(v3::moderation::ResultCount { count: count.count }), session_queue, ) .await - .or_else(v2_reroute::flatten_404_error) + .or_else(v2_reroute::flatten_404_error)?; + + // Convert to V2 projects + match v2_reroute::extract_ok_json::>(response).await { + Ok(project) => { + let legacy_projects = LegacyProject::from_many(project, &**pool, &redis).await?; + Ok(HttpResponse::Ok().json(legacy_projects)) + } + Err(response) => Ok(response), + } } diff --git a/src/routes/v2/notifications.rs b/src/routes/v2/notifications.rs index ab58ee1a3..a6df95310 100644 --- a/src/routes/v2/notifications.rs +++ b/src/routes/v2/notifications.rs @@ -85,6 +85,7 @@ pub async fn notification_read( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert v3::notifications::notification_read(req, info, pool, redis, session_queue) .await .or_else(v2_reroute::flatten_404_error) @@ -98,6 +99,7 @@ pub async fn notification_delete( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert v3::notifications::notification_delete(req, info, pool, redis, session_queue) .await .or_else(v2_reroute::flatten_404_error) @@ -111,6 +113,7 @@ pub async fn notifications_read( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert v3::notifications::notifications_read( req, web::Query(v3::notifications::NotificationIds { ids: ids.ids }), @@ -130,6 +133,7 @@ pub async fn notifications_delete( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert v3::notifications::notifications_delete( req, web::Query(v3::notifications::NotificationIds { ids: ids.ids }), diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index c6237cb49..eb171ac63 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -2,11 +2,10 @@ use crate::database::models::categories::LinkPlatform; use crate::database::models::{project_item, version_item}; use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; -use crate::models; use crate::models::projects::{ Link, MonetizationStatus, Project, ProjectStatus, SearchRequest, Version, }; -use crate::models::v2::projects::{DonationLink, LegacyProject, LegacySideType}; +use crate::models::v2::projects::{DonationLink, LegacyProject, LegacySideType, LegacyVersion}; use crate::models::v2::search::LegacySearchResults; use crate::queue::session::AuthQueue; use crate::routes::v3::projects::ProjectIds; @@ -223,6 +222,7 @@ pub async fn project_get_check( pool: web::Data, redis: web::Data, ) -> Result { + // Returns an id only, do not need to convert v3::projects::project_get_check(info, pool, redis) .await .or_else(v2_reroute::flatten_404_error) @@ -230,8 +230,8 @@ pub async fn project_get_check( #[derive(Serialize)] struct DependencyInfo { - pub projects: Vec, - pub versions: Vec, + pub projects: Vec, + pub versions: Vec, } #[get("dependencies")] @@ -242,10 +242,30 @@ pub async fn dependency_list( redis: web::Data, session_queue: web::Data, ) -> Result { - // TODO: requires V2 conversion and tests, probably - v3::projects::dependency_list(req, info, pool, redis, session_queue) - .await - .or_else(v2_reroute::flatten_404_error) + // TODO: tests, probably + let response = + v3::projects::dependency_list(req, info, pool.clone(), redis.clone(), session_queue) + .await + .or_else(v2_reroute::flatten_404_error)?; + + match v2_reroute::extract_ok_json::(response).await + { + Ok(dependency_info) => { + let converted_projects = + LegacyProject::from_many(dependency_info.projects, &**pool, &redis).await?; + let converted_versions = dependency_info + .versions + .into_iter() + .map(LegacyVersion::from) + .collect(); + + Ok(HttpResponse::Ok().json(DependencyInfo { + projects: converted_projects, + versions: converted_versions, + })) + } + Err(response) => Ok(response), + } } #[derive(Serialize, Deserialize, Validate)] @@ -636,6 +656,7 @@ pub async fn projects_edit( } } + // This returns NoContent or failure so we don't need to do anything with it v3::projects::projects_edit( req, web::Query(ids), @@ -673,6 +694,7 @@ pub async fn project_icon_edit( payload: web::Payload, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert v3::projects::project_icon_edit( web::Query(v3::projects::Extension { ext: ext.ext }), req, @@ -696,6 +718,7 @@ pub async fn delete_project_icon( file_host: web::Data>, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert v3::projects::delete_project_icon(req, info, pool, redis, file_host, session_queue) .await .or_else(v2_reroute::flatten_404_error) @@ -724,6 +747,7 @@ pub async fn add_gallery_item( payload: web::Payload, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert v3::projects::add_gallery_item( web::Query(v3::projects::Extension { ext: ext.ext }), req, @@ -775,6 +799,7 @@ pub async fn edit_gallery_item( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert v3::projects::edit_gallery_item( req, web::Query(v3::projects::GalleryEditQuery { @@ -808,6 +833,7 @@ pub async fn delete_gallery_item( file_host: web::Data>, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert v3::projects::delete_gallery_item( req, web::Query(v3::projects::GalleryDeleteQuery { url: item.url }), @@ -830,6 +856,7 @@ pub async fn project_delete( config: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert v3::projects::project_delete(req, info, pool, redis, config, session_queue) .await .or_else(v2_reroute::flatten_404_error) @@ -843,6 +870,7 @@ pub async fn project_follow( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert v3::projects::project_follow(req, info, pool, redis, session_queue) .await .or_else(v2_reroute::flatten_404_error) @@ -856,6 +884,7 @@ pub async fn project_unfollow( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert v3::projects::project_unfollow(req, info, pool, redis, session_queue) .await .or_else(v2_reroute::flatten_404_error) diff --git a/src/routes/v2/reports.rs b/src/routes/v2/reports.rs index 80e481c89..7f92522e7 100644 --- a/src/routes/v2/reports.rs +++ b/src/routes/v2/reports.rs @@ -1,6 +1,7 @@ use crate::database::redis::RedisPool; use crate::models::ids::ImageId; -use crate::models::reports::ItemType; +use crate::models::reports::{ItemType, Report}; +use crate::models::v2::reports::LegacyReport; use crate::queue::session::AuthQueue; use crate::routes::{v2_reroute, v3, ApiError}; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; @@ -37,9 +38,18 @@ pub async fn report_create( redis: web::Data, session_queue: web::Data, ) -> Result { - v3::reports::report_create(req, pool, body, redis, session_queue) + let response = v3::reports::report_create(req, pool, body, redis, session_queue) .await - .or_else(v2_reroute::flatten_404_error) + .or_else(v2_reroute::flatten_404_error)?; + + // Convert response to V2 format + match v2_reroute::extract_ok_json::(response).await { + Ok(report) => { + let report = LegacyReport::from(report); + Ok(HttpResponse::Ok().json(report)) + } + Err(response) => Ok(response), + } } #[derive(Deserialize)] @@ -65,7 +75,7 @@ pub async fn reports( count: web::Query, session_queue: web::Data, ) -> Result { - v3::reports::reports( + let response = v3::reports::reports( req, pool, redis, @@ -76,7 +86,16 @@ pub async fn reports( session_queue, ) .await - .or_else(v2_reroute::flatten_404_error) + .or_else(v2_reroute::flatten_404_error)?; + + // Convert response to V2 format + match v2_reroute::extract_ok_json::>(response).await { + Ok(reports) => { + let reports: Vec<_> = reports.into_iter().map(LegacyReport::from).collect(); + Ok(HttpResponse::Ok().json(reports)) + } + Err(response) => Ok(response), + } } #[derive(Deserialize)] @@ -92,7 +111,7 @@ pub async fn reports_get( redis: web::Data, session_queue: web::Data, ) -> Result { - v3::reports::reports_get( + let response = v3::reports::reports_get( req, web::Query(v3::reports::ReportIds { ids: ids.ids }), pool, @@ -100,7 +119,16 @@ pub async fn reports_get( session_queue, ) .await - .or_else(v2_reroute::flatten_404_error) + .or_else(v2_reroute::flatten_404_error)?; + + // Convert response to V2 format + match v2_reroute::extract_ok_json::>(response).await { + Ok(report_list) => { + let report_list: Vec<_> = report_list.into_iter().map(LegacyReport::from).collect(); + Ok(HttpResponse::Ok().json(report_list)) + } + Err(response) => Ok(response), + } } #[get("report/{id}")] @@ -111,9 +139,18 @@ pub async fn report_get( info: web::Path<(crate::models::reports::ReportId,)>, session_queue: web::Data, ) -> Result { - v3::reports::report_get(req, pool, redis, info, session_queue) + let response = v3::reports::report_get(req, pool, redis, info, session_queue) .await - .or_else(v2_reroute::flatten_404_error) + .or_else(v2_reroute::flatten_404_error)?; + + // Convert response to V2 format + match v2_reroute::extract_ok_json::(response).await { + Ok(report) => { + let report = LegacyReport::from(report); + Ok(HttpResponse::Ok().json(report)) + } + Err(response) => Ok(response), + } } #[derive(Deserialize, Validate)] @@ -133,6 +170,7 @@ pub async fn report_edit( edit_report: web::Json, ) -> Result { let edit_report = edit_report.into_inner(); + // Returns NoContent, so no need to convert v3::reports::report_edit( req, pool, @@ -156,6 +194,7 @@ pub async fn report_delete( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert v3::reports::report_delete(req, pool, info, redis, session_queue) .await .or_else(v2_reroute::flatten_404_error) diff --git a/src/routes/v2/statistics.rs b/src/routes/v2/statistics.rs index 514dc5a51..b5f5b7817 100644 --- a/src/routes/v2/statistics.rs +++ b/src/routes/v2/statistics.rs @@ -1,4 +1,8 @@ -use crate::routes::{v2_reroute, v3, ApiError}; +use crate::routes::{ + v2_reroute, + v3::{self, statistics::V3Stats}, + ApiError, +}; use actix_web::{get, web, HttpResponse}; use sqlx::PgPool; @@ -6,9 +10,30 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(get_stats); } +#[derive(serde::Serialize)] +pub struct V2Stats { + pub projects: Option, + pub versions: Option, + pub authors: Option, + pub files: Option, +} + #[get("statistics")] pub async fn get_stats(pool: web::Data) -> Result { - v3::statistics::get_stats(pool) + let response = v3::statistics::get_stats(pool) .await - .or_else(v2_reroute::flatten_404_error) + .or_else(v2_reroute::flatten_404_error)?; + + match v2_reroute::extract_ok_json::(response).await { + Ok(stats) => { + let stats = V2Stats { + projects: stats.projects, + versions: stats.versions, + authors: stats.authors, + files: stats.files, + }; + Ok(HttpResponse::Ok().json(stats)) + } + Err(response) => Ok(response), + } } diff --git a/src/routes/v2/tags.rs b/src/routes/v2/tags.rs index 2713d3551..c99c69362 100644 --- a/src/routes/v2/tags.rs +++ b/src/routes/v2/tags.rs @@ -5,9 +5,7 @@ use crate::database::models::loader_fields::LoaderFieldEnumValue; use crate::database::redis::RedisPool; use crate::models::v2::projects::LegacySideType; use crate::routes::v2_reroute::capitalize_first; -use crate::routes::v3::tags::{ - LinkPlatformQueryData, LoaderData as LoaderDataV3, LoaderFieldsEnumQuery, -}; +use crate::routes::v3::tags::{LinkPlatformQueryData, LoaderFieldsEnumQuery}; use crate::routes::{v2_reroute, v3}; use actix_web::{get, web, HttpResponse}; use chrono::{DateTime, Utc}; @@ -42,7 +40,24 @@ pub async fn category_list( pool: web::Data, redis: web::Data, ) -> Result { - v3::tags::category_list(pool, redis).await + let response = v3::tags::category_list(pool, redis).await?; + + // Convert to V2 format + match v2_reroute::extract_ok_json::>(response).await { + Ok(categories) => { + let categories = categories + .into_iter() + .map(|c| CategoryData { + icon: c.icon, + name: c.name, + project_type: c.project_type, + header: c.header, + }) + .collect::>(); + Ok(HttpResponse::Ok().json(categories)) + } + Err(response) => Ok(response), + } } #[derive(serde::Serialize, serde::Deserialize)] @@ -60,7 +75,7 @@ pub async fn loader_list( let response = v3::tags::loader_list(pool, redis).await?; // Convert to V2 format - match v2_reroute::extract_ok_json::>(response).await { + match v2_reroute::extract_ok_json::>(response).await { Ok(loaders) => { let loaders = loaders .into_iter() @@ -151,26 +166,52 @@ pub async fn game_version_list( #[derive(serde::Serialize)] pub struct License { - short: String, - name: String, + pub short: String, + pub name: String, } #[get("license")] pub async fn license_list() -> HttpResponse { - v3::tags::license_list().await + let response = v3::tags::license_list().await; + + // Convert to V2 format + match v2_reroute::extract_ok_json::>(response).await { + Ok(licenses) => { + let licenses = licenses + .into_iter() + .map(|l| License { + short: l.short, + name: l.name, + }) + .collect::>(); + HttpResponse::Ok().json(licenses) + } + Err(response) => response, + } } #[derive(serde::Serialize)] pub struct LicenseText { - title: String, - body: String, + pub title: String, + pub body: String, } #[get("license/{id}")] pub async fn license_text(params: web::Path<(String,)>) -> Result { - v3::tags::license_text(params) + let license = v3::tags::license_text(params) .await - .or_else(v2_reroute::flatten_404_error) + .or_else(v2_reroute::flatten_404_error)?; + + // Convert to V2 format + Ok( + match v2_reroute::extract_ok_json::(license).await { + Ok(license) => HttpResponse::Ok().json(LicenseText { + title: license.title, + body: license.body, + }), + Err(response) => response, + }, + ) } #[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)] @@ -229,6 +270,7 @@ pub async fn report_type_list( pool: web::Data, redis: web::Data, ) -> Result { + // This returns a list of strings directly, so we don't need to convert to v2 format. v3::tags::report_type_list(pool, redis) .await .or_else(v2_reroute::flatten_404_error) @@ -239,6 +281,7 @@ pub async fn project_type_list( pool: web::Data, redis: web::Data, ) -> Result { + // This returns a list of strings directly, so we don't need to convert to v2 format. v3::tags::project_type_list(pool, redis) .await .or_else(v2_reroute::flatten_404_error) diff --git a/src/routes/v2/teams.rs b/src/routes/v2/teams.rs index 5ff2a9e0a..e6dfe2b7d 100644 --- a/src/routes/v2/teams.rs +++ b/src/routes/v2/teams.rs @@ -124,6 +124,7 @@ pub async fn join_team( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so we don't need to convert the response v3::teams::join_team(req, info, pool, redis, session_queue) .await .or_else(v2_reroute::flatten_404_error) @@ -162,6 +163,7 @@ pub async fn add_team_member( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so we don't need to convert the response v3::teams::add_team_member( req, info, @@ -199,6 +201,7 @@ pub async fn edit_team_member( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so we don't need to convert the response v3::teams::edit_team_member( req, info, @@ -231,6 +234,7 @@ pub async fn transfer_ownership( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so we don't need to convert the response v3::teams::transfer_ownership( req, info, @@ -253,6 +257,7 @@ pub async fn remove_team_member( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so we don't need to convert the response v3::teams::remove_team_member(req, info, pool, redis, session_queue) .await .or_else(v2_reroute::flatten_404_error) diff --git a/src/routes/v2/threads.rs b/src/routes/v2/threads.rs index deb3e2402..fd0c2ec6c 100644 --- a/src/routes/v2/threads.rs +++ b/src/routes/v2/threads.rs @@ -3,7 +3,8 @@ use std::sync::Arc; use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; use crate::models::ids::ThreadMessageId; -use crate::models::threads::{MessageBody, ThreadId}; +use crate::models::threads::{MessageBody, Thread, ThreadId}; +use crate::models::v2::threads::LegacyThread; use crate::queue::session::AuthQueue; use crate::routes::{v2_reroute, v3, ApiError}; use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; @@ -48,7 +49,7 @@ pub async fn threads_get( redis: web::Data, session_queue: web::Data, ) -> Result { - v3::threads::threads_get( + let response = v3::threads::threads_get( req, web::Query(v3::threads::ThreadIds { ids: ids.ids }), pool, @@ -56,7 +57,19 @@ pub async fn threads_get( session_queue, ) .await - .or_else(v2_reroute::flatten_404_error) + .or_else(v2_reroute::flatten_404_error)?; + + // Convert response to V2 format + match v2_reroute::extract_ok_json::>(response).await { + Ok(threads) => { + let threads = threads + .into_iter() + .map(LegacyThread::from) + .collect::>(); + Ok(HttpResponse::Ok().json(threads)) + } + Err(response) => Ok(response), + } } #[derive(Deserialize)] @@ -74,6 +87,7 @@ pub async fn thread_send_message( session_queue: web::Data, ) -> Result { let new_message = new_message.into_inner(); + // Returns NoContent, so we don't need to convert the response v3::threads::thread_send_message( req, info, @@ -95,9 +109,21 @@ pub async fn moderation_inbox( redis: web::Data, session_queue: web::Data, ) -> Result { - v3::threads::moderation_inbox(req, pool, redis, session_queue) + let response = v3::threads::moderation_inbox(req, pool, redis, session_queue) .await - .or_else(v2_reroute::flatten_404_error) + .or_else(v2_reroute::flatten_404_error)?; + + // Convert response to V2 format + match v2_reroute::extract_ok_json::>(response).await { + Ok(threads) => { + let threads = threads + .into_iter() + .map(LegacyThread::from) + .collect::>(); + Ok(HttpResponse::Ok().json(threads)) + } + Err(response) => Ok(response), + } } #[post("{id}/read")] @@ -108,6 +134,7 @@ pub async fn thread_read( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so we don't need to convert the response v3::threads::thread_read(req, info, pool, redis, session_queue) .await .or_else(v2_reroute::flatten_404_error) @@ -122,6 +149,7 @@ pub async fn message_delete( session_queue: web::Data, file_host: web::Data>, ) -> Result { + // Returns NoContent, so we don't need to convert the response v3::threads::message_delete(req, info, pool, redis, session_queue, file_host) .await .or_else(v2_reroute::flatten_404_error) diff --git a/src/routes/v2/users.rs b/src/routes/v2/users.rs index 6d2d60ab7..cf747c79b 100644 --- a/src/routes/v2/users.rs +++ b/src/routes/v2/users.rs @@ -2,9 +2,10 @@ use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; use crate::models::notifications::Notification; use crate::models::projects::Project; -use crate::models::users::{Badges, Role}; +use crate::models::users::{Badges, Role, User}; use crate::models::v2::notifications::LegacyNotification; use crate::models::v2::projects::LegacyProject; +use crate::models::v2::user::LegacyUser; use crate::queue::session::AuthQueue; use crate::routes::{v2_reroute, v3, ApiError}; use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; @@ -38,9 +39,18 @@ pub async fn user_auth_get( redis: web::Data, session_queue: web::Data, ) -> Result { - v3::users::user_auth_get(req, pool, redis, session_queue) + let response = v3::users::user_auth_get(req, pool, redis, session_queue) .await - .or_else(v2_reroute::flatten_404_error) + .or_else(v2_reroute::flatten_404_error)?; + + // Convert response to V2 format + match v2_reroute::extract_ok_json::(response).await { + Ok(user) => { + let user = LegacyUser::from(user); + Ok(HttpResponse::Ok().json(user)) + } + Err(response) => Ok(response), + } } #[derive(Serialize, Deserialize)] @@ -54,9 +64,19 @@ pub async fn users_get( pool: web::Data, redis: web::Data, ) -> Result { - v3::users::users_get(web::Query(v3::users::UserIds { ids: ids.ids }), pool, redis) - .await - .or_else(v2_reroute::flatten_404_error) + let response = + v3::users::users_get(web::Query(v3::users::UserIds { ids: ids.ids }), pool, redis) + .await + .or_else(v2_reroute::flatten_404_error)?; + + // Convert response to V2 format + match v2_reroute::extract_ok_json::>(response).await { + Ok(users) => { + let legacy_users: Vec = users.into_iter().map(LegacyUser::from).collect(); + Ok(HttpResponse::Ok().json(legacy_users)) + } + Err(response) => Ok(response), + } } #[get("{id}")] @@ -65,9 +85,18 @@ pub async fn user_get( pool: web::Data, redis: web::Data, ) -> Result { - v3::users::user_get(info, pool, redis) + let response = v3::users::user_get(info, pool, redis) .await - .or_else(v2_reroute::flatten_404_error) + .or_else(v2_reroute::flatten_404_error)?; + + // Convert response to V2 format + match v2_reroute::extract_ok_json::(response).await { + Ok(user) => { + let user = LegacyUser::from(user); + Ok(HttpResponse::Ok().json(user)) + } + Err(response) => Ok(response), + } } #[get("{user_id}/projects")] @@ -128,6 +157,7 @@ pub async fn user_edit( session_queue: web::Data, ) -> Result { let new_user = new_user.into_inner(); + // Returns NoContent, so we don't need to convert to V2 v3::users::user_edit( req, info, @@ -164,6 +194,7 @@ pub async fn user_icon_edit( payload: web::Payload, session_queue: web::Data, ) -> Result { + // Returns NoContent, so we don't need to convert to V2 v3::users::user_icon_edit( web::Query(v3::users::Extension { ext: ext.ext }), req, @@ -186,6 +217,7 @@ pub async fn user_delete( redis: web::Data, session_queue: web::Data, ) -> Result { + // Returns NoContent, so we don't need to convert to V2 v3::users::user_delete(req, info, pool, redis, session_queue) .await .or_else(v2_reroute::flatten_404_error) @@ -199,9 +231,18 @@ pub async fn user_follows( redis: web::Data, session_queue: web::Data, ) -> Result { - v3::users::user_follows(req, info, pool, redis, session_queue) + let response = v3::users::user_follows(req, info, pool.clone(), redis.clone(), session_queue) .await - .or_else(v2_reroute::flatten_404_error) + .or_else(v2_reroute::flatten_404_error)?; + + // Convert to V2 projects + match v2_reroute::extract_ok_json::>(response).await { + Ok(project) => { + let legacy_projects = LegacyProject::from_many(project, &**pool, &redis).await?; + Ok(HttpResponse::Ok().json(legacy_projects)) + } + Err(response) => Ok(response), + } } #[get("{id}/notifications")] diff --git a/src/routes/v2/version_creation.rs b/src/routes/v2/version_creation.rs index 0c7bffb4d..3ea1497bc 100644 --- a/src/routes/v2/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -268,6 +268,7 @@ pub async fn upload_file_to_version( file_host: Data>, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert to V2 let response = v3::version_creation::upload_file_to_version( req, url_data, diff --git a/src/routes/v2/version_file.rs b/src/routes/v2/version_file.rs index a50d25b42..05224fe64 100644 --- a/src/routes/v2/version_file.rs +++ b/src/routes/v2/version_file.rs @@ -63,6 +63,7 @@ pub async fn download_version( hash_query: web::Query, session_queue: web::Data, ) -> Result { + // Returns TemporaryRedirect, so no need to convert to V2 v3::version_file::download_version(req, info, pool, redis, hash_query, session_queue) .await .or_else(v2_reroute::flatten_404_error) @@ -78,6 +79,7 @@ pub async fn delete_file( hash_query: web::Query, session_queue: web::Data, ) -> Result { + // Returns NoContent, so no need to convert to V2 v3::version_file::delete_file(req, info, pool, redis, hash_query, session_queue) .await .or_else(v2_reroute::flatten_404_error) diff --git a/src/routes/v2/versions.rs b/src/routes/v2/versions.rs index 94468f34f..9054201f1 100644 --- a/src/routes/v2/versions.rs +++ b/src/routes/v2/versions.rs @@ -270,6 +270,7 @@ pub async fn version_delete( session_queue: web::Data, search_config: web::Data, ) -> Result { + // Returns NoContent, so we don't need to convert the response v3::versions::version_delete(req, info, pool, redis, session_queue, search_config) .await .or_else(v2_reroute::flatten_404_error) @@ -290,6 +291,7 @@ pub async fn version_schedule( scheduling_data: web::Json, session_queue: web::Data, ) -> Result { + // Returns NoContent, so we don't need to convert the response let scheduling_data = scheduling_data.into_inner(); let scheduling_data = v3::versions::SchedulingData { time: scheduling_data.time, diff --git a/src/routes/v3/projects.rs b/src/routes/v3/projects.rs index 0cca89386..1e9a72ff0 100644 --- a/src/routes/v3/projects.rs +++ b/src/routes/v3/projects.rs @@ -939,8 +939,8 @@ pub async fn project_get_check( } } -#[derive(Serialize)] -struct DependencyInfo { +#[derive(Serialize, Deserialize)] +pub struct DependencyInfo { pub projects: Vec, pub versions: Vec, } diff --git a/src/routes/v3/statistics.rs b/src/routes/v3/statistics.rs index 7a0def234..c6c24e1a3 100644 --- a/src/routes/v3/statistics.rs +++ b/src/routes/v3/statistics.rs @@ -1,12 +1,19 @@ use crate::routes::ApiError; use actix_web::{web, HttpResponse}; -use serde_json::json; use sqlx::PgPool; pub fn config(cfg: &mut web::ServiceConfig) { cfg.route("statistics", web::get().to(get_stats)); } +#[derive(serde::Serialize, serde::Deserialize)] +pub struct V3Stats { + pub projects: Option, + pub versions: Option, + pub authors: Option, + pub files: Option, +} + pub async fn get_stats(pool: web::Data) -> Result { let projects = sqlx::query!( " @@ -74,12 +81,12 @@ pub async fn get_stats(pool: web::Data) -> Result HttpResponse { @@ -186,10 +186,10 @@ pub async fn license_list() -> HttpResponse { HttpResponse::Ok().json(results) } -#[derive(serde::Serialize)] +#[derive(serde::Serialize, serde::Deserialize)] pub struct LicenseText { - title: String, - body: String, + pub title: String, + pub body: String, } pub async fn license_text(params: web::Path<(String,)>) -> Result { diff --git a/tests/common/api_common/models.rs b/tests/common/api_common/models.rs index e8776f2c8..acfd3042e 100644 --- a/tests/common/api_common/models.rs +++ b/tests/common/api_common/models.rs @@ -1,22 +1,30 @@ use chrono::{DateTime, Utc}; -use labrinth::models::{ - notifications::NotificationId, - organizations::OrganizationId, - projects::{ - Dependency, GalleryItem, License, ModeratorMessage, MonetizationStatus, ProjectId, - ProjectStatus, VersionFile, VersionId, VersionStatus, VersionType, +use labrinth::{ + auth::AuthProvider, + models::{ + images::ImageId, + notifications::NotificationId, + organizations::OrganizationId, + projects::{ + Dependency, GalleryItem, License, ModeratorMessage, MonetizationStatus, ProjectId, + ProjectStatus, VersionFile, VersionId, VersionStatus, VersionType, + }, + reports::ReportId, + teams::{ProjectPermissions, TeamId}, + threads::{ThreadId, ThreadMessageId}, + users::{Badges, Role, User, UserId, UserPayoutData}, }, - teams::{ProjectPermissions, TeamId}, - threads::ThreadId, - users::{User, UserId}, }; use rust_decimal::Decimal; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; // Fields shared by every version of the API. // No struct in here should have ANY field that // is not present in *every* version of the API. +// Exceptions are fields that *should* be changing across the API, and older versions +// should be unsupported on API version increase- for example, payouts related financial fields. + // These are used for common tests- tests that can be used on both V2 AND v3 of the API and have the same results. // Any test that requires version-specific fields should have its own test that is not done for each version, @@ -120,7 +128,7 @@ pub struct CommonNotificationAction { pub action_route: (String, String), } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Deserialize, Clone)] #[serde(rename_all = "kebab-case")] pub enum CommonItemType { Project, @@ -139,3 +147,88 @@ impl CommonItemType { } } } + +#[derive(Deserialize)] +pub struct CommonReport { + pub id: ReportId, + pub report_type: String, + pub item_id: String, + pub item_type: CommonItemType, + pub reporter: UserId, + pub body: String, + pub created: DateTime, + pub closed: bool, + pub thread_id: ThreadId, +} + +#[derive(Deserialize)] +pub enum LegacyItemType { + Project, + Version, + User, + Unknown, +} + +#[derive(Deserialize)] +pub struct CommonThread { + pub id: ThreadId, + #[serde(rename = "type")] + pub type_: CommonThreadType, + pub project_id: Option, + pub report_id: Option, + pub messages: Vec, + pub members: Vec, +} + +#[derive(Deserialize)] +pub struct CommonThreadMessage { + pub id: ThreadMessageId, + pub author_id: Option, + pub body: CommonMessageBody, + pub created: DateTime, +} + +#[derive(Deserialize)] +pub enum CommonMessageBody { + Text { + body: String, + #[serde(default)] + private: bool, + replying_to: Option, + #[serde(default)] + associated_images: Vec, + }, + StatusChange { + new_status: ProjectStatus, + old_status: ProjectStatus, + }, + ThreadClosure, + ThreadReopen, + Deleted, +} + +#[derive(Deserialize)] +pub enum CommonThreadType { + Report, + Project, + DirectMessage, +} + +#[derive(Deserialize)] +pub struct CommonUser { + pub id: UserId, + pub username: String, + pub name: Option, + pub avatar_url: Option, + pub bio: Option, + pub created: DateTime, + pub role: Role, + pub badges: Badges, + pub auth_providers: Option>, + pub email: Option, + pub email_verified: Option, + pub has_password: Option, + pub has_totp: Option, + pub payout_data: Option, + pub github_id: Option, +}