Sanity checked all of V2 route conversions (#803)

* follows

* all v2 routes now either convert or have a comment

* added common structs, clippy

* merge fix

---------

Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Wyatt Verchere 2023-12-19 10:20:32 -08:00 committed by GitHub
parent 9f798559cf
commit d59c522f7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 636 additions and 78 deletions

View File

@ -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;

52
src/models/v2/reports.rs Normal file
View File

@ -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<Utc>,
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<ItemType> 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<Report> 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,
}
}
}

120
src/models/v2/threads.rs Normal file
View File

@ -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<ProjectId>,
pub report_id: Option<ReportId>,
pub messages: Vec<LegacyThreadMessage>,
pub members: Vec<User>,
}
#[derive(Serialize, Deserialize)]
pub struct LegacyThreadMessage {
pub id: ThreadMessageId,
pub author_id: Option<UserId>,
pub body: LegacyMessageBody,
pub created: DateTime<Utc>,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum LegacyMessageBody {
Text {
body: String,
#[serde(default)]
private: bool,
replying_to: Option<ThreadMessageId>,
#[serde(default)]
associated_images: Vec<ImageId>,
},
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<crate::models::v3::threads::ThreadType> 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<crate::models::v3::threads::MessageBody> 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<crate::models::v3::threads::ThreadMessage> 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<crate::models::v3::threads::Thread> 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,
}
}
}

53
src/models/v2/user.rs Normal file
View File

@ -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<String>,
pub avatar_url: Option<String>,
pub bio: Option<String>,
pub created: DateTime<Utc>,
pub role: Role,
pub badges: Badges,
pub auth_providers: Option<Vec<AuthProvider>>, // this was changed in v3, but not changes ones we want to keep out of v2
pub email: Option<String>,
pub email_verified: Option<bool>,
pub has_password: Option<bool>,
pub has_totp: Option<bool>,
pub payout_data: Option<UserPayoutData>, // this was changed in v3, but not ones we want to keep out of v2
// DEPRECATED. Always returns None
pub github_id: Option<u64>,
}
impl From<crate::models::v3::users::User> 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,
}
}
}

View File

@ -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<ResultCount>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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::<Vec<Project>>(response).await {
Ok(project) => {
let legacy_projects = LegacyProject::from_many(project, &**pool, &redis).await?;
Ok(HttpResponse::Ok().json(legacy_projects))
}
Err(response) => Ok(response),
}
}

View File

@ -85,6 +85,7 @@ pub async fn notification_read(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// Returns NoContent, so no need to convert
v3::notifications::notifications_delete(
req,
web::Query(v3::notifications::NotificationIds { ids: ids.ids }),

View File

@ -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<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
// 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<Project>,
pub versions: Vec<models::projects::Version>,
pub projects: Vec<LegacyProject>,
pub versions: Vec<LegacyVersion>,
}
#[get("dependencies")]
@ -242,10 +242,30 @@ pub async fn dependency_list(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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::<crate::routes::v3::projects::DependencyInfo>(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<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<Arc<dyn FileHost + Send + Sync>>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<Arc<dyn FileHost + Send + Sync>>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<SearchConfig>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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)

View File

@ -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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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::<Report>(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<ReportsRequestOptions>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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::<Vec<Report>>(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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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::<Vec<Report>>(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<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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::<Report>(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<EditReport>,
) -> Result<HttpResponse, ApiError> {
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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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)

View File

@ -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<i64>,
pub versions: Option<i64>,
pub authors: Option<i64>,
pub files: Option<i64>,
}
#[get("statistics")]
pub async fn get_stats(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
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::<V3Stats>(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),
}
}

View File

@ -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<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
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::<Vec<v3::tags::CategoryData>>(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::<Vec<_>>();
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::<Vec<LoaderDataV3>>(response).await {
match v2_reroute::extract_ok_json::<Vec<v3::tags::LoaderData>>(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::<Vec<v3::tags::License>>(response).await {
Ok(licenses) => {
let licenses = licenses
.into_iter()
.map(|l| License {
short: l.short,
name: l.name,
})
.collect::<Vec<_>>();
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<HttpResponse, ApiError> {
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::<v3::tags::LicenseText>(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<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
// 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<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
// 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)

View File

@ -124,6 +124,7 @@ pub async fn join_team(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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)

View File

@ -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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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::<Vec<Thread>>(response).await {
Ok(threads) => {
let threads = threads
.into_iter()
.map(LegacyThread::from)
.collect::<Vec<_>>();
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<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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::<Vec<Thread>>(response).await {
Ok(threads) => {
let threads = threads
.into_iter()
.map(LegacyThread::from)
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(threads))
}
Err(response) => Ok(response),
}
}
#[post("{id}/read")]
@ -108,6 +134,7 @@ pub async fn thread_read(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<AuthQueue>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
) -> Result<HttpResponse, ApiError> {
// 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)

View File

@ -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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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::<User>(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<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
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::<Vec<User>>(response).await {
Ok(users) => {
let legacy_users: Vec<LegacyUser> = 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<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
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::<User>(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<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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::<Vec<Project>>(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")]

View File

@ -268,6 +268,7 @@ pub async fn upload_file_to_version(
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, CreateError> {
// Returns NoContent, so no need to convert to V2
let response = v3::version_creation::upload_file_to_version(
req,
url_data,

View File

@ -63,6 +63,7 @@ pub async fn download_version(
hash_query: web::Query<HashQuery>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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<HashQuery>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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)

View File

@ -270,6 +270,7 @@ pub async fn version_delete(
session_queue: web::Data<AuthQueue>,
search_config: web::Data<SearchConfig>,
) -> Result<HttpResponse, ApiError> {
// 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<SchedulingData>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// 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,

View File

@ -939,8 +939,8 @@ pub async fn project_get_check(
}
}
#[derive(Serialize)]
struct DependencyInfo {
#[derive(Serialize, Deserialize)]
pub struct DependencyInfo {
pub projects: Vec<Project>,
pub versions: Vec<models::projects::Version>,
}

View File

@ -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<i64>,
pub versions: Option<i64>,
pub authors: Option<i64>,
pub files: Option<i64>,
}
pub async fn get_stats(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
let projects = sqlx::query!(
"
@ -74,12 +81,12 @@ pub async fn get_stats(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError
.fetch_one(&**pool)
.await?;
let json = json!({
"projects": projects.count,
"versions": versions.count,
"authors": authors.count,
"files": files.count,
});
let v3_stats = V3Stats {
projects: projects.count,
versions: versions.count,
authors: authors.count,
files: files.count,
};
Ok(HttpResponse::Ok().json(json))
Ok(HttpResponse::Ok().json(v3_stats))
}

View File

@ -166,10 +166,10 @@ pub async fn loader_fields_list(
Ok(HttpResponse::Ok().json(results))
}
#[derive(serde::Serialize)]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct License {
short: String,
name: String,
pub short: String,
pub name: String,
}
pub async fn license_list() -> 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<HttpResponse, ApiError> {

View File

@ -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<Utc>,
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<ProjectId>,
pub report_id: Option<ReportId>,
pub messages: Vec<CommonThreadMessage>,
pub members: Vec<User>,
}
#[derive(Deserialize)]
pub struct CommonThreadMessage {
pub id: ThreadMessageId,
pub author_id: Option<UserId>,
pub body: CommonMessageBody,
pub created: DateTime<Utc>,
}
#[derive(Deserialize)]
pub enum CommonMessageBody {
Text {
body: String,
#[serde(default)]
private: bool,
replying_to: Option<ThreadMessageId>,
#[serde(default)]
associated_images: Vec<ImageId>,
},
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<String>,
pub avatar_url: Option<String>,
pub bio: Option<String>,
pub created: DateTime<Utc>,
pub role: Role,
pub badges: Badges,
pub auth_providers: Option<Vec<AuthProvider>>,
pub email: Option<String>,
pub email_verified: Option<bool>,
pub has_password: Option<bool>,
pub has_totp: Option<bool>,
pub payout_data: Option<UserPayoutData>,
pub github_id: Option<u64>,
}