You've already forked AstralRinth
forked from didirus/AstralRinth
move to monorepo dir
This commit is contained in:
8
apps/labrinth/src/models/v2/mod.rs
Normal file
8
apps/labrinth/src/models/v2/mod.rs
Normal file
@@ -0,0 +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;
|
||||
184
apps/labrinth/src/models/v2/notifications.rs
Normal file
184
apps/labrinth/src/models/v2/notifications.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::models::{
|
||||
ids::{
|
||||
NotificationId, OrganizationId, ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId,
|
||||
UserId, VersionId,
|
||||
},
|
||||
notifications::{Notification, NotificationAction, NotificationBody},
|
||||
projects::ProjectStatus,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LegacyNotification {
|
||||
pub id: NotificationId,
|
||||
pub user_id: UserId,
|
||||
pub read: bool,
|
||||
pub created: DateTime<Utc>,
|
||||
pub body: LegacyNotificationBody,
|
||||
|
||||
// DEPRECATED: use body field instead
|
||||
#[serde(rename = "type")]
|
||||
pub type_: Option<String>,
|
||||
pub title: String,
|
||||
pub text: String,
|
||||
pub link: String,
|
||||
pub actions: Vec<LegacyNotificationAction>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct LegacyNotificationAction {
|
||||
pub title: String,
|
||||
/// The route to call when this notification action is called. Formatted HTTP Method, route
|
||||
pub action_route: (String, String),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum LegacyNotificationBody {
|
||||
ProjectUpdate {
|
||||
project_id: ProjectId,
|
||||
version_id: VersionId,
|
||||
},
|
||||
TeamInvite {
|
||||
project_id: ProjectId,
|
||||
team_id: TeamId,
|
||||
invited_by: UserId,
|
||||
role: String,
|
||||
},
|
||||
OrganizationInvite {
|
||||
organization_id: OrganizationId,
|
||||
invited_by: UserId,
|
||||
team_id: TeamId,
|
||||
role: String,
|
||||
},
|
||||
StatusChange {
|
||||
project_id: ProjectId,
|
||||
old_status: ProjectStatus,
|
||||
new_status: ProjectStatus,
|
||||
},
|
||||
ModeratorMessage {
|
||||
thread_id: ThreadId,
|
||||
message_id: ThreadMessageId,
|
||||
|
||||
project_id: Option<ProjectId>,
|
||||
report_id: Option<ReportId>,
|
||||
},
|
||||
LegacyMarkdown {
|
||||
notification_type: Option<String>,
|
||||
title: String,
|
||||
text: String,
|
||||
link: String,
|
||||
actions: Vec<NotificationAction>,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl LegacyNotification {
|
||||
pub fn from(notification: Notification) -> Self {
|
||||
let type_ = match ¬ification.body {
|
||||
NotificationBody::ProjectUpdate { .. } => Some("project_update".to_string()),
|
||||
NotificationBody::TeamInvite { .. } => Some("team_invite".to_string()),
|
||||
NotificationBody::OrganizationInvite { .. } => Some("organization_invite".to_string()),
|
||||
NotificationBody::StatusChange { .. } => Some("status_change".to_string()),
|
||||
NotificationBody::ModeratorMessage { .. } => Some("moderator_message".to_string()),
|
||||
NotificationBody::LegacyMarkdown {
|
||||
notification_type, ..
|
||||
} => notification_type.clone(),
|
||||
NotificationBody::Unknown => None,
|
||||
};
|
||||
|
||||
let legacy_body = match notification.body {
|
||||
NotificationBody::ProjectUpdate {
|
||||
project_id,
|
||||
version_id,
|
||||
} => LegacyNotificationBody::ProjectUpdate {
|
||||
project_id,
|
||||
version_id,
|
||||
},
|
||||
NotificationBody::TeamInvite {
|
||||
project_id,
|
||||
team_id,
|
||||
invited_by,
|
||||
role,
|
||||
} => LegacyNotificationBody::TeamInvite {
|
||||
project_id,
|
||||
team_id,
|
||||
invited_by,
|
||||
role,
|
||||
},
|
||||
NotificationBody::OrganizationInvite {
|
||||
organization_id,
|
||||
invited_by,
|
||||
team_id,
|
||||
role,
|
||||
} => LegacyNotificationBody::OrganizationInvite {
|
||||
organization_id,
|
||||
invited_by,
|
||||
team_id,
|
||||
role,
|
||||
},
|
||||
NotificationBody::StatusChange {
|
||||
project_id,
|
||||
old_status,
|
||||
new_status,
|
||||
} => LegacyNotificationBody::StatusChange {
|
||||
project_id,
|
||||
old_status,
|
||||
new_status,
|
||||
},
|
||||
NotificationBody::ModeratorMessage {
|
||||
thread_id,
|
||||
message_id,
|
||||
project_id,
|
||||
report_id,
|
||||
} => LegacyNotificationBody::ModeratorMessage {
|
||||
thread_id,
|
||||
message_id,
|
||||
project_id,
|
||||
report_id,
|
||||
},
|
||||
NotificationBody::LegacyMarkdown {
|
||||
notification_type,
|
||||
name,
|
||||
text,
|
||||
link,
|
||||
actions,
|
||||
} => LegacyNotificationBody::LegacyMarkdown {
|
||||
notification_type,
|
||||
title: name,
|
||||
text,
|
||||
link,
|
||||
actions,
|
||||
},
|
||||
NotificationBody::Unknown => LegacyNotificationBody::Unknown,
|
||||
};
|
||||
|
||||
Self {
|
||||
id: notification.id,
|
||||
user_id: notification.user_id,
|
||||
read: notification.read,
|
||||
created: notification.created,
|
||||
body: legacy_body,
|
||||
type_,
|
||||
title: notification.name,
|
||||
text: notification.text,
|
||||
link: notification.link,
|
||||
actions: notification
|
||||
.actions
|
||||
.into_iter()
|
||||
.map(LegacyNotificationAction::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LegacyNotificationAction {
|
||||
pub fn from(notification_action: NotificationAction) -> Self {
|
||||
Self {
|
||||
title: notification_action.name,
|
||||
action_route: notification_action.action_route,
|
||||
}
|
||||
}
|
||||
}
|
||||
405
apps/labrinth/src/models/v2/projects.rs
Normal file
405
apps/labrinth/src/models/v2/projects.rs
Normal file
@@ -0,0 +1,405 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::super::ids::OrganizationId;
|
||||
use super::super::teams::TeamId;
|
||||
use super::super::users::UserId;
|
||||
use crate::database::models::{version_item, DatabaseError};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::ids::{ProjectId, VersionId};
|
||||
use crate::models::projects::{
|
||||
Dependency, License, Link, Loader, ModeratorMessage, MonetizationStatus, Project,
|
||||
ProjectStatus, Version, VersionFile, VersionStatus, VersionType,
|
||||
};
|
||||
use crate::models::threads::ThreadId;
|
||||
use crate::routes::v2_reroute::{self, capitalize_first};
|
||||
use chrono::{DateTime, Utc};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// A project returned from the API
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct LegacyProject {
|
||||
/// Relevant V2 fields- these were removed or modfified in V3,
|
||||
/// and are now part of the dynamic fields system
|
||||
/// The support range for the client project*
|
||||
pub client_side: LegacySideType,
|
||||
/// The support range for the server project
|
||||
pub server_side: LegacySideType,
|
||||
/// A list of game versions this project supports
|
||||
pub game_versions: Vec<String>,
|
||||
|
||||
// All other fields are the same as V3
|
||||
// If they change, or their constituent types change, we may need to
|
||||
// add a new struct for them here.
|
||||
pub id: ProjectId,
|
||||
pub slug: Option<String>,
|
||||
pub project_type: String,
|
||||
pub team: TeamId,
|
||||
pub organization: Option<OrganizationId>,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub body: String,
|
||||
pub body_url: Option<String>,
|
||||
pub published: DateTime<Utc>,
|
||||
pub updated: DateTime<Utc>,
|
||||
pub approved: Option<DateTime<Utc>>,
|
||||
pub queued: Option<DateTime<Utc>>,
|
||||
pub status: ProjectStatus,
|
||||
pub requested_status: Option<ProjectStatus>,
|
||||
pub moderator_message: Option<ModeratorMessage>,
|
||||
pub license: License,
|
||||
pub downloads: u32,
|
||||
pub followers: u32,
|
||||
pub categories: Vec<String>,
|
||||
pub additional_categories: Vec<String>,
|
||||
pub loaders: Vec<String>,
|
||||
pub versions: Vec<VersionId>,
|
||||
pub icon_url: Option<String>,
|
||||
pub issues_url: Option<String>,
|
||||
pub source_url: Option<String>,
|
||||
pub wiki_url: Option<String>,
|
||||
pub discord_url: Option<String>,
|
||||
pub donation_urls: Option<Vec<DonationLink>>,
|
||||
pub gallery: Vec<LegacyGalleryItem>,
|
||||
pub color: Option<u32>,
|
||||
pub thread_id: ThreadId,
|
||||
pub monetization_status: MonetizationStatus,
|
||||
}
|
||||
|
||||
impl LegacyProject {
|
||||
// Returns visible v2 project_type and also 'og' selected project type
|
||||
// These are often identical, but we want to display 'mod' for datapacks and plugins
|
||||
// The latter can be used for further processing, such as determining side types of plugins
|
||||
pub fn get_project_type(project_types: &[String]) -> (String, String) {
|
||||
// V2 versions only have one project type- v3 versions can rarely have multiple.
|
||||
// We'll prioritize 'modpack' first, and if neither are found, use the first one.
|
||||
// If there are no project types, default to 'project'
|
||||
let mut project_types = project_types.to_vec();
|
||||
if project_types.contains(&"modpack".to_string()) {
|
||||
project_types = vec!["modpack".to_string()];
|
||||
}
|
||||
|
||||
let og_project_type = project_types
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or("project".to_string()); // Default to 'project' if none are found
|
||||
|
||||
let project_type = if og_project_type == "datapack" || og_project_type == "plugin" {
|
||||
// These are not supported in V2, so we'll just use 'mod' instead
|
||||
"mod".to_string()
|
||||
} else {
|
||||
og_project_type.clone()
|
||||
};
|
||||
|
||||
(project_type, og_project_type)
|
||||
}
|
||||
|
||||
// Convert from a standard V3 project to a V2 project
|
||||
// Requires any queried versions to be passed in, to get access to certain version fields contained within.
|
||||
// - This can be any version, because the fields are ones that used to be on the project itself.
|
||||
// - Its conceivable that certain V3 projects that have many different ones may not have the same fields on all of them.
|
||||
// It's safe to use a db version_item for this as the only info is side types, game versions, and loader fields (for loaders), which used to be public on project anyway.
|
||||
pub fn from(data: Project, versions_item: Option<version_item::QueryVersion>) -> Self {
|
||||
let mut client_side = LegacySideType::Unknown;
|
||||
let mut server_side = LegacySideType::Unknown;
|
||||
|
||||
// V2 versions only have one project type- v3 versions can rarely have multiple.
|
||||
// We'll prioritize 'modpack' first, and if neither are found, use the first one.
|
||||
// If there are no project types, default to 'project'
|
||||
let project_types = data.project_types;
|
||||
let (mut project_type, og_project_type) = Self::get_project_type(&project_types);
|
||||
|
||||
let mut loaders = data.loaders;
|
||||
|
||||
let game_versions = data
|
||||
.fields
|
||||
.get("game_versions")
|
||||
.unwrap_or(&Vec::new())
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|v| v.to_string())
|
||||
.collect();
|
||||
|
||||
if let Some(versions_item) = versions_item {
|
||||
// Extract side types from remaining fields (singleplayer, client_only, etc)
|
||||
let fields = versions_item
|
||||
.version_fields
|
||||
.iter()
|
||||
.map(|f| (f.field_name.clone(), f.value.clone().serialize_internal()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
(client_side, server_side) =
|
||||
v2_reroute::convert_side_types_v2(&fields, Some(&*og_project_type));
|
||||
|
||||
// - if loader is mrpack, this is a modpack
|
||||
// the loaders are whatever the corresponding loader fields are
|
||||
if loaders.contains(&"mrpack".to_string()) {
|
||||
project_type = "modpack".to_string();
|
||||
if let Some(mrpack_loaders) = data.fields.iter().find(|f| f.0 == "mrpack_loaders") {
|
||||
let values = mrpack_loaders
|
||||
.1
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|v| v.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// drop mrpack from loaders
|
||||
loaders = loaders
|
||||
.into_iter()
|
||||
.filter(|l| l != "mrpack")
|
||||
.collect::<Vec<_>>();
|
||||
// and replace with mrpack_loaders
|
||||
loaders.extend(values);
|
||||
// remove duplicate loaders
|
||||
loaders = loaders.into_iter().unique().collect::<Vec<_>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let issues_url = data.link_urls.get("issues").map(|l| l.url.clone());
|
||||
let source_url = data.link_urls.get("source").map(|l| l.url.clone());
|
||||
let wiki_url = data.link_urls.get("wiki").map(|l| l.url.clone());
|
||||
let discord_url = data.link_urls.get("discord").map(|l| l.url.clone());
|
||||
|
||||
let donation_urls = data
|
||||
.link_urls
|
||||
.iter()
|
||||
.filter(|(_, l)| l.donation)
|
||||
.map(|(_, l)| DonationLink::try_from(l.clone()).ok())
|
||||
.collect::<Option<Vec<_>>>();
|
||||
|
||||
Self {
|
||||
id: data.id,
|
||||
slug: data.slug,
|
||||
project_type,
|
||||
team: data.team_id,
|
||||
organization: data.organization,
|
||||
title: data.name,
|
||||
description: data.summary, // V2 description is V3 summary
|
||||
body: data.description, // V2 body is V3 description
|
||||
body_url: None, // Always None even in V2
|
||||
published: data.published,
|
||||
updated: data.updated,
|
||||
approved: data.approved,
|
||||
queued: data.queued,
|
||||
status: data.status,
|
||||
requested_status: data.requested_status,
|
||||
moderator_message: data.moderator_message,
|
||||
license: data.license,
|
||||
downloads: data.downloads,
|
||||
followers: data.followers,
|
||||
categories: data.categories,
|
||||
additional_categories: data.additional_categories,
|
||||
loaders,
|
||||
versions: data.versions,
|
||||
icon_url: data.icon_url,
|
||||
issues_url,
|
||||
source_url,
|
||||
wiki_url,
|
||||
discord_url,
|
||||
donation_urls,
|
||||
gallery: data
|
||||
.gallery
|
||||
.into_iter()
|
||||
.map(LegacyGalleryItem::from)
|
||||
.collect(),
|
||||
color: data.color,
|
||||
thread_id: data.thread_id,
|
||||
monetization_status: data.monetization_status,
|
||||
client_side,
|
||||
server_side,
|
||||
game_versions,
|
||||
}
|
||||
}
|
||||
|
||||
// Because from needs a version_item, this is a helper function to get many from one db query.
|
||||
pub async fn from_many<'a, E>(
|
||||
data: Vec<Project>,
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<Self>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Acquire<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let version_ids: Vec<_> = data
|
||||
.iter()
|
||||
.filter_map(|p| p.versions.first().map(|i| (*i).into()))
|
||||
.collect();
|
||||
let example_versions = version_item::Version::get_many(&version_ids, exec, redis).await?;
|
||||
let mut legacy_projects = Vec::new();
|
||||
for project in data {
|
||||
let version_item = example_versions
|
||||
.iter()
|
||||
.find(|v| v.inner.project_id == project.id.into())
|
||||
.cloned();
|
||||
let project = LegacyProject::from(project, version_item);
|
||||
legacy_projects.push(project);
|
||||
}
|
||||
Ok(legacy_projects)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Copy)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum LegacySideType {
|
||||
Required,
|
||||
Optional,
|
||||
Unsupported,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LegacySideType {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl LegacySideType {
|
||||
// These are constant, so this can remove unneccessary allocations (`to_string`)
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
LegacySideType::Required => "required",
|
||||
LegacySideType::Optional => "optional",
|
||||
LegacySideType::Unsupported => "unsupported",
|
||||
LegacySideType::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> LegacySideType {
|
||||
match string {
|
||||
"required" => LegacySideType::Required,
|
||||
"optional" => LegacySideType::Optional,
|
||||
"unsupported" => LegacySideType::Unsupported,
|
||||
_ => LegacySideType::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A specific version of a project
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct LegacyVersion {
|
||||
/// Relevant V2 fields- these were removed or modfified in V3,
|
||||
/// and are now part of the dynamic fields system
|
||||
/// A list of game versions this project supports
|
||||
pub game_versions: Vec<String>,
|
||||
|
||||
/// A list of loaders this project supports (has a newtype struct)
|
||||
pub loaders: Vec<Loader>,
|
||||
|
||||
pub id: VersionId,
|
||||
pub project_id: ProjectId,
|
||||
pub author_id: UserId,
|
||||
pub featured: bool,
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
pub changelog: String,
|
||||
pub changelog_url: Option<String>,
|
||||
pub date_published: DateTime<Utc>,
|
||||
pub downloads: u32,
|
||||
pub version_type: VersionType,
|
||||
pub status: VersionStatus,
|
||||
pub requested_status: Option<VersionStatus>,
|
||||
pub files: Vec<VersionFile>,
|
||||
pub dependencies: Vec<Dependency>,
|
||||
}
|
||||
|
||||
impl From<Version> for LegacyVersion {
|
||||
fn from(data: Version) -> Self {
|
||||
let mut game_versions = Vec::new();
|
||||
if let Some(value) = data.fields.get("game_versions").and_then(|v| v.as_array()) {
|
||||
for gv in value {
|
||||
if let Some(game_version) = gv.as_str() {
|
||||
game_versions.push(game_version.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// - if loader is mrpack, this is a modpack
|
||||
// the v2 loaders are whatever the corresponding loader fields are
|
||||
let mut loaders = data.loaders.into_iter().map(|l| l.0).collect::<Vec<_>>();
|
||||
if loaders.contains(&"mrpack".to_string()) {
|
||||
if let Some((_, mrpack_loaders)) = data
|
||||
.fields
|
||||
.into_iter()
|
||||
.find(|(key, _)| key == "mrpack_loaders")
|
||||
{
|
||||
if let Ok(mrpack_loaders) = serde_json::from_value(mrpack_loaders) {
|
||||
loaders = mrpack_loaders;
|
||||
}
|
||||
}
|
||||
}
|
||||
let loaders = loaders.into_iter().map(Loader).collect::<Vec<_>>();
|
||||
|
||||
Self {
|
||||
id: data.id,
|
||||
project_id: data.project_id,
|
||||
author_id: data.author_id,
|
||||
featured: data.featured,
|
||||
name: data.name,
|
||||
version_number: data.version_number,
|
||||
changelog: data.changelog,
|
||||
changelog_url: None, // Always None even in V2
|
||||
date_published: data.date_published,
|
||||
downloads: data.downloads,
|
||||
version_type: data.version_type,
|
||||
status: data.status,
|
||||
requested_status: data.requested_status,
|
||||
files: data.files,
|
||||
dependencies: data.dependencies,
|
||||
game_versions,
|
||||
loaders,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct LegacyGalleryItem {
|
||||
pub url: String,
|
||||
pub raw_url: String,
|
||||
pub featured: bool,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub created: DateTime<Utc>,
|
||||
pub ordering: i64,
|
||||
}
|
||||
|
||||
impl LegacyGalleryItem {
|
||||
fn from(data: crate::models::projects::GalleryItem) -> Self {
|
||||
Self {
|
||||
url: data.url,
|
||||
raw_url: data.raw_url,
|
||||
featured: data.featured,
|
||||
title: data.name,
|
||||
description: data.description,
|
||||
created: data.created,
|
||||
ordering: data.ordering,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate, Clone, Eq, PartialEq)]
|
||||
pub struct DonationLink {
|
||||
pub id: String,
|
||||
pub platform: String,
|
||||
#[validate(
|
||||
custom(function = "crate::util::validate::validate_url"),
|
||||
length(max = 2048)
|
||||
)]
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl TryFrom<Link> for DonationLink {
|
||||
type Error = String;
|
||||
fn try_from(link: Link) -> Result<Self, String> {
|
||||
if !link.donation {
|
||||
return Err("Not a donation".to_string());
|
||||
}
|
||||
Ok(Self {
|
||||
platform: capitalize_first(&link.platform),
|
||||
url: link.url,
|
||||
id: link.platform,
|
||||
})
|
||||
}
|
||||
}
|
||||
52
apps/labrinth/src/models/v2/reports.rs
Normal file
52
apps/labrinth/src/models/v2/reports.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
178
apps/labrinth/src/models/v2/search.rs
Normal file
178
apps/labrinth/src/models/v2/search.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{routes::v2_reroute, search::ResultSearchProject};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LegacySearchResults {
|
||||
pub hits: Vec<LegacyResultSearchProject>,
|
||||
pub offset: usize,
|
||||
pub limit: usize,
|
||||
pub total_hits: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LegacyResultSearchProject {
|
||||
pub project_id: String,
|
||||
pub project_type: String,
|
||||
pub slug: Option<String>,
|
||||
pub author: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub categories: Vec<String>,
|
||||
pub display_categories: Vec<String>,
|
||||
pub versions: Vec<String>,
|
||||
pub downloads: i32,
|
||||
pub follows: i32,
|
||||
pub icon_url: String,
|
||||
/// RFC 3339 formatted creation date of the project
|
||||
pub date_created: String,
|
||||
/// RFC 3339 formatted modification date of the project
|
||||
pub date_modified: String,
|
||||
pub latest_version: String,
|
||||
pub license: String,
|
||||
pub client_side: String,
|
||||
pub server_side: String,
|
||||
pub gallery: Vec<String>,
|
||||
pub featured_gallery: Option<String>,
|
||||
pub color: Option<u32>,
|
||||
}
|
||||
|
||||
// TODO: In other PR, when these are merged, make sure the v2 search testing functions use these
|
||||
impl LegacyResultSearchProject {
|
||||
pub fn from(result_search_project: ResultSearchProject) -> Self {
|
||||
let mut categories = result_search_project.categories;
|
||||
categories.extend(result_search_project.loaders.clone());
|
||||
if categories.contains(&"mrpack".to_string()) {
|
||||
if let Some(mrpack_loaders) = result_search_project
|
||||
.project_loader_fields
|
||||
.get("mrpack_loaders")
|
||||
{
|
||||
categories.extend(
|
||||
mrpack_loaders
|
||||
.iter()
|
||||
.filter_map(|c| c.as_str())
|
||||
.map(String::from),
|
||||
);
|
||||
categories.retain(|c| c != "mrpack");
|
||||
}
|
||||
}
|
||||
let mut display_categories = result_search_project.display_categories;
|
||||
display_categories.extend(result_search_project.loaders);
|
||||
if display_categories.contains(&"mrpack".to_string()) {
|
||||
if let Some(mrpack_loaders) = result_search_project
|
||||
.project_loader_fields
|
||||
.get("mrpack_loaders")
|
||||
{
|
||||
categories.extend(
|
||||
mrpack_loaders
|
||||
.iter()
|
||||
.filter_map(|c| c.as_str())
|
||||
.map(String::from),
|
||||
);
|
||||
display_categories.retain(|c| c != "mrpack");
|
||||
}
|
||||
}
|
||||
|
||||
// Sort then remove duplicates
|
||||
categories.sort();
|
||||
categories.dedup();
|
||||
display_categories.sort();
|
||||
display_categories.dedup();
|
||||
|
||||
// V2 versions only have one project type- v3 versions can rarely have multiple.
|
||||
// We'll prioritize 'modpack' first, and if neither are found, use the first one.
|
||||
// If there are no project types, default to 'project'
|
||||
let mut project_types = result_search_project.project_types;
|
||||
if project_types.contains(&"modpack".to_string()) {
|
||||
project_types = vec!["modpack".to_string()];
|
||||
}
|
||||
let og_project_type = project_types
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or("project".to_string()); // Default to 'project' if none are found
|
||||
|
||||
let project_type = if og_project_type == "datapack" || og_project_type == "plugin" {
|
||||
// These are not supported in V2, so we'll just use 'mod' instead
|
||||
"mod".to_string()
|
||||
} else {
|
||||
og_project_type.clone()
|
||||
};
|
||||
|
||||
let project_loader_fields = result_search_project.project_loader_fields.clone();
|
||||
let get_one_bool_loader_field = |key: &str| {
|
||||
project_loader_fields
|
||||
.get(key)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.first()
|
||||
.and_then(|s| s.as_bool())
|
||||
};
|
||||
|
||||
let singleplayer = get_one_bool_loader_field("singleplayer");
|
||||
let client_only = get_one_bool_loader_field("client_only").unwrap_or(false);
|
||||
let server_only = get_one_bool_loader_field("server_only").unwrap_or(false);
|
||||
let client_and_server = get_one_bool_loader_field("client_and_server");
|
||||
|
||||
let (client_side, server_side) = v2_reroute::convert_side_types_v2_bools(
|
||||
singleplayer,
|
||||
client_only,
|
||||
server_only,
|
||||
client_and_server,
|
||||
Some(&*og_project_type),
|
||||
);
|
||||
let client_side = client_side.to_string();
|
||||
let server_side = server_side.to_string();
|
||||
|
||||
let versions = result_search_project
|
||||
.project_loader_fields
|
||||
.get("game_versions")
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|s| s.as_str().map(String::from))
|
||||
.collect_vec();
|
||||
|
||||
Self {
|
||||
project_type,
|
||||
client_side,
|
||||
server_side,
|
||||
versions,
|
||||
latest_version: result_search_project.version_id,
|
||||
categories,
|
||||
|
||||
project_id: result_search_project.project_id,
|
||||
slug: result_search_project.slug,
|
||||
author: result_search_project.author,
|
||||
title: result_search_project.name,
|
||||
description: result_search_project.summary,
|
||||
display_categories,
|
||||
downloads: result_search_project.downloads,
|
||||
follows: result_search_project.follows,
|
||||
icon_url: result_search_project.icon_url.unwrap_or_default(),
|
||||
license: result_search_project.license,
|
||||
date_created: result_search_project.date_created,
|
||||
date_modified: result_search_project.date_modified,
|
||||
gallery: result_search_project.gallery,
|
||||
featured_gallery: result_search_project.featured_gallery,
|
||||
color: result_search_project.color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LegacySearchResults {
|
||||
pub fn from(search_results: crate::search::SearchResults) -> Self {
|
||||
let limit = search_results.hits_per_page;
|
||||
let offset = (search_results.page - 1) * limit;
|
||||
Self {
|
||||
hits: search_results
|
||||
.hits
|
||||
.into_iter()
|
||||
.map(LegacyResultSearchProject::from)
|
||||
.collect(),
|
||||
offset,
|
||||
limit,
|
||||
total_hits: search_results.total_hits,
|
||||
}
|
||||
}
|
||||
}
|
||||
41
apps/labrinth/src/models/v2/teams.rs
Normal file
41
apps/labrinth/src/models/v2/teams.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::models::{
|
||||
ids::TeamId,
|
||||
teams::{ProjectPermissions, TeamMember},
|
||||
users::User,
|
||||
};
|
||||
|
||||
/// A member of a team
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct LegacyTeamMember {
|
||||
pub role: String,
|
||||
// is_owner removed, and role hardcoded to Owner if true,
|
||||
pub team_id: TeamId,
|
||||
pub user: User,
|
||||
pub permissions: Option<ProjectPermissions>,
|
||||
pub accepted: bool,
|
||||
|
||||
#[serde(with = "rust_decimal::serde::float_option")]
|
||||
pub payouts_split: Option<Decimal>,
|
||||
pub ordering: i64,
|
||||
}
|
||||
|
||||
impl LegacyTeamMember {
|
||||
pub fn from(team_member: TeamMember) -> Self {
|
||||
LegacyTeamMember {
|
||||
role: match (team_member.is_owner, team_member.role.as_str()) {
|
||||
(true, _) => "Owner".to_string(),
|
||||
(false, "Owner") => "Member".to_string(), // The odd case of a non-owner with the owner role should show as 'Member'
|
||||
(false, role) => role.to_string(),
|
||||
},
|
||||
team_id: team_member.team_id,
|
||||
user: team_member.user,
|
||||
permissions: team_member.permissions,
|
||||
accepted: team_member.accepted,
|
||||
payouts_split: team_member.payouts_split,
|
||||
ordering: team_member.ordering,
|
||||
}
|
||||
}
|
||||
}
|
||||
125
apps/labrinth/src/models/v2/threads.rs
Normal file
125
apps/labrinth/src/models/v2/threads.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
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 {
|
||||
#[serde(default)]
|
||||
private: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[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 { private } => {
|
||||
LegacyMessageBody::Deleted { private }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
apps/labrinth/src/models/v2/user.rs
Normal file
53
apps/labrinth/src/models/v2/user.rs
Normal 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: None,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user