You've already forked AstralRinth
forked from didirus/AstralRinth
move to monorepo dir
This commit is contained in:
8
apps/labrinth/src/models/error.rs
Normal file
8
apps/labrinth/src/models/error.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An error returned by the API
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ApiError<'a> {
|
||||
pub error: &'a str,
|
||||
pub description: String,
|
||||
}
|
||||
21
apps/labrinth/src/models/mod.rs
Normal file
21
apps/labrinth/src/models/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
pub mod error;
|
||||
pub mod v2;
|
||||
pub mod v3;
|
||||
|
||||
pub use v3::analytics;
|
||||
pub use v3::billing;
|
||||
pub use v3::collections;
|
||||
pub use v3::ids;
|
||||
pub use v3::images;
|
||||
pub use v3::notifications;
|
||||
pub use v3::oauth_clients;
|
||||
pub use v3::organizations;
|
||||
pub use v3::pack;
|
||||
pub use v3::pats;
|
||||
pub use v3::payouts;
|
||||
pub use v3::projects;
|
||||
pub use v3::reports;
|
||||
pub use v3::sessions;
|
||||
pub use v3::teams;
|
||||
pub use v3::threads;
|
||||
pub use v3::users;
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
64
apps/labrinth/src/models/v3/analytics.rs
Normal file
64
apps/labrinth/src/models/v3/analytics.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use clickhouse::Row;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::hash::Hash;
|
||||
use std::net::Ipv6Addr;
|
||||
|
||||
#[derive(Row, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Download {
|
||||
pub recorded: i64,
|
||||
pub domain: String,
|
||||
pub site_path: String,
|
||||
|
||||
// Modrinth User ID for logged in users, default 0
|
||||
pub user_id: u64,
|
||||
// default is 0 if unknown
|
||||
pub project_id: u64,
|
||||
// default is 0 if unknown
|
||||
pub version_id: u64,
|
||||
|
||||
// The below information is used exclusively for data aggregation and fraud detection
|
||||
// (ex: download botting).
|
||||
pub ip: Ipv6Addr,
|
||||
pub country: String,
|
||||
pub user_agent: String,
|
||||
pub headers: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
#[derive(Row, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct PageView {
|
||||
pub recorded: i64,
|
||||
pub domain: String,
|
||||
pub site_path: String,
|
||||
|
||||
// Modrinth User ID for logged in users
|
||||
pub user_id: u64,
|
||||
// Modrinth Project ID (used for payouts)
|
||||
pub project_id: u64,
|
||||
// whether this view will be monetized / counted for payouts
|
||||
pub monetized: bool,
|
||||
|
||||
// The below information is used exclusively for data aggregation and fraud detection
|
||||
// (ex: page view botting).
|
||||
pub ip: Ipv6Addr,
|
||||
pub country: String,
|
||||
pub user_agent: String,
|
||||
pub headers: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
#[derive(Row, Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct Playtime {
|
||||
pub recorded: i64,
|
||||
pub seconds: u64,
|
||||
|
||||
// Modrinth User ID for logged in users (unused atm)
|
||||
pub user_id: u64,
|
||||
// Modrinth Project ID
|
||||
pub project_id: u64,
|
||||
// Modrinth Version ID
|
||||
pub version_id: u64,
|
||||
|
||||
pub loader: String,
|
||||
pub game_version: String,
|
||||
/// Parent modpack this playtime was recorded in
|
||||
pub parent: u64,
|
||||
}
|
||||
232
apps/labrinth/src/models/v3/billing.rs
Normal file
232
apps/labrinth/src/models/v3/billing.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
use crate::models::ids::Base62Id;
|
||||
use crate::models::ids::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct ProductId(pub u64);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Product {
|
||||
pub id: ProductId,
|
||||
pub metadata: ProductMetadata,
|
||||
pub prices: Vec<ProductPrice>,
|
||||
pub unitary: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum ProductMetadata {
|
||||
Midas,
|
||||
Pyro {
|
||||
cpu: u32,
|
||||
ram: u32,
|
||||
swap: u32,
|
||||
storage: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct ProductPriceId(pub u64);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ProductPrice {
|
||||
pub id: ProductPriceId,
|
||||
pub product_id: ProductId,
|
||||
pub prices: Price,
|
||||
pub currency_code: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum Price {
|
||||
OneTime {
|
||||
price: i32,
|
||||
},
|
||||
Recurring {
|
||||
intervals: HashMap<PriceDuration, i32>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Debug, Copy, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PriceDuration {
|
||||
Monthly,
|
||||
Yearly,
|
||||
}
|
||||
|
||||
impl PriceDuration {
|
||||
pub fn duration(&self) -> chrono::Duration {
|
||||
match self {
|
||||
PriceDuration::Monthly => chrono::Duration::days(30),
|
||||
PriceDuration::Yearly => chrono::Duration::days(365),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> PriceDuration {
|
||||
match string {
|
||||
"monthly" => PriceDuration::Monthly,
|
||||
"yearly" => PriceDuration::Yearly,
|
||||
_ => PriceDuration::Monthly,
|
||||
}
|
||||
}
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
PriceDuration::Monthly => "monthly",
|
||||
PriceDuration::Yearly => "yearly",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iterator() -> impl Iterator<Item = PriceDuration> {
|
||||
vec![PriceDuration::Monthly, PriceDuration::Yearly].into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct UserSubscriptionId(pub u64);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct UserSubscription {
|
||||
pub id: UserSubscriptionId,
|
||||
pub user_id: UserId,
|
||||
pub price_id: ProductPriceId,
|
||||
pub interval: PriceDuration,
|
||||
pub status: SubscriptionStatus,
|
||||
pub created: DateTime<Utc>,
|
||||
pub metadata: Option<SubscriptionMetadata>,
|
||||
}
|
||||
|
||||
impl From<crate::database::models::user_subscription_item::UserSubscriptionItem>
|
||||
for UserSubscription
|
||||
{
|
||||
fn from(x: crate::database::models::user_subscription_item::UserSubscriptionItem) -> Self {
|
||||
Self {
|
||||
id: x.id.into(),
|
||||
user_id: x.user_id.into(),
|
||||
price_id: x.price_id.into(),
|
||||
interval: x.interval,
|
||||
status: x.status,
|
||||
created: x.created,
|
||||
metadata: x.metadata,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SubscriptionStatus {
|
||||
Provisioned,
|
||||
Unprovisioned,
|
||||
}
|
||||
|
||||
impl SubscriptionStatus {
|
||||
pub fn from_string(string: &str) -> SubscriptionStatus {
|
||||
match string {
|
||||
"provisioned" => SubscriptionStatus::Provisioned,
|
||||
"unprovisioned" => SubscriptionStatus::Unprovisioned,
|
||||
_ => SubscriptionStatus::Provisioned,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
SubscriptionStatus::Provisioned => "provisioned",
|
||||
SubscriptionStatus::Unprovisioned => "unprovisioned",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum SubscriptionMetadata {
|
||||
Pyro { id: String },
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct ChargeId(pub u64);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Charge {
|
||||
pub id: ChargeId,
|
||||
pub user_id: UserId,
|
||||
pub price_id: ProductPriceId,
|
||||
pub amount: i64,
|
||||
pub currency_code: String,
|
||||
pub status: ChargeStatus,
|
||||
pub due: DateTime<Utc>,
|
||||
pub last_attempt: Option<DateTime<Utc>>,
|
||||
#[serde(flatten)]
|
||||
pub type_: ChargeType,
|
||||
pub subscription_id: Option<UserSubscriptionId>,
|
||||
pub subscription_interval: Option<PriceDuration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum ChargeType {
|
||||
OneTime,
|
||||
Subscription,
|
||||
Proration,
|
||||
}
|
||||
|
||||
impl ChargeType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ChargeType::OneTime => "one-time",
|
||||
ChargeType::Subscription { .. } => "subscription",
|
||||
ChargeType::Proration { .. } => "proration",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> ChargeType {
|
||||
match string {
|
||||
"one-time" => ChargeType::OneTime,
|
||||
"subscription" => ChargeType::Subscription,
|
||||
"proration" => ChargeType::Proration,
|
||||
_ => ChargeType::OneTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ChargeStatus {
|
||||
// Open charges are for the next billing interval
|
||||
Open,
|
||||
Processing,
|
||||
Succeeded,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl ChargeStatus {
|
||||
pub fn from_string(string: &str) -> ChargeStatus {
|
||||
match string {
|
||||
"processing" => ChargeStatus::Processing,
|
||||
"succeeded" => ChargeStatus::Succeeded,
|
||||
"failed" => ChargeStatus::Failed,
|
||||
"open" => ChargeStatus::Open,
|
||||
"cancelled" => ChargeStatus::Cancelled,
|
||||
_ => ChargeStatus::Failed,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ChargeStatus::Processing => "processing",
|
||||
ChargeStatus::Succeeded => "succeeded",
|
||||
ChargeStatus::Failed => "failed",
|
||||
ChargeStatus::Open => "open",
|
||||
ChargeStatus::Cancelled => "cancelled",
|
||||
}
|
||||
}
|
||||
}
|
||||
132
apps/labrinth/src/models/v3/collections.rs
Normal file
132
apps/labrinth/src/models/v3/collections.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use super::{
|
||||
ids::{Base62Id, ProjectId},
|
||||
users::UserId,
|
||||
};
|
||||
use crate::database;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The ID of a specific collection, encoded as base62 for usage in the API
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct CollectionId(pub u64);
|
||||
|
||||
/// A collection returned from the API
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Collection {
|
||||
/// The ID of the collection, encoded as a base62 string.
|
||||
pub id: CollectionId,
|
||||
/// The person that has ownership of this collection.
|
||||
pub user: UserId,
|
||||
/// The title or name of the collection.
|
||||
pub name: String,
|
||||
/// A short description of the collection.
|
||||
pub description: Option<String>,
|
||||
|
||||
/// An icon URL for the collection.
|
||||
pub icon_url: Option<String>,
|
||||
/// Color of the collection.
|
||||
pub color: Option<u32>,
|
||||
|
||||
/// The status of the collectin (eg: whether collection is public or not)
|
||||
pub status: CollectionStatus,
|
||||
|
||||
/// The date at which the collection was first published.
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// The date at which the collection was updated.
|
||||
pub updated: DateTime<Utc>,
|
||||
|
||||
/// A list of ProjectIds that are in this collection.
|
||||
pub projects: Vec<ProjectId>,
|
||||
}
|
||||
|
||||
impl From<database::models::Collection> for Collection {
|
||||
fn from(c: database::models::Collection) -> Self {
|
||||
Self {
|
||||
id: c.id.into(),
|
||||
user: c.user_id.into(),
|
||||
created: c.created,
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
updated: c.updated,
|
||||
projects: c.projects.into_iter().map(|x| x.into()).collect(),
|
||||
icon_url: c.icon_url,
|
||||
color: c.color,
|
||||
status: c.status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A status decides the visibility of a collection in search, URLs, and the whole site itself.
|
||||
/// Listed - collection is displayed on search, and accessible by URL (for if/when search is implemented for collections)
|
||||
/// Unlisted - collection is not displayed on search, but accessible by URL
|
||||
/// Rejected - collection is disabled
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CollectionStatus {
|
||||
Listed,
|
||||
Unlisted,
|
||||
Private,
|
||||
Rejected,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CollectionStatus {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl CollectionStatus {
|
||||
pub fn from_string(string: &str) -> CollectionStatus {
|
||||
match string {
|
||||
"listed" => CollectionStatus::Listed,
|
||||
"unlisted" => CollectionStatus::Unlisted,
|
||||
"private" => CollectionStatus::Private,
|
||||
"rejected" => CollectionStatus::Rejected,
|
||||
_ => CollectionStatus::Unknown,
|
||||
}
|
||||
}
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
CollectionStatus::Listed => "listed",
|
||||
CollectionStatus::Unlisted => "unlisted",
|
||||
CollectionStatus::Private => "private",
|
||||
CollectionStatus::Rejected => "rejected",
|
||||
CollectionStatus::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
// Project pages + info cannot be viewed
|
||||
pub fn is_hidden(&self) -> bool {
|
||||
match self {
|
||||
CollectionStatus::Rejected => true,
|
||||
CollectionStatus::Private => true,
|
||||
CollectionStatus::Listed => false,
|
||||
CollectionStatus::Unlisted => false,
|
||||
CollectionStatus::Unknown => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_approved(&self) -> bool {
|
||||
match self {
|
||||
CollectionStatus::Listed => true,
|
||||
CollectionStatus::Private => true,
|
||||
CollectionStatus::Unlisted => true,
|
||||
CollectionStatus::Rejected => false,
|
||||
CollectionStatus::Unknown => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_be_requested(&self) -> bool {
|
||||
match self {
|
||||
CollectionStatus::Listed => true,
|
||||
CollectionStatus::Private => true,
|
||||
CollectionStatus::Unlisted => true,
|
||||
CollectionStatus::Rejected => false,
|
||||
CollectionStatus::Unknown => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
222
apps/labrinth/src/models/v3/ids.rs
Normal file
222
apps/labrinth/src/models/v3/ids.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
pub use super::collections::CollectionId;
|
||||
pub use super::images::ImageId;
|
||||
pub use super::notifications::NotificationId;
|
||||
pub use super::oauth_clients::OAuthClientAuthorizationId;
|
||||
pub use super::oauth_clients::{OAuthClientId, OAuthRedirectUriId};
|
||||
pub use super::organizations::OrganizationId;
|
||||
pub use super::pats::PatId;
|
||||
pub use super::payouts::PayoutId;
|
||||
pub use super::projects::{ProjectId, VersionId};
|
||||
pub use super::reports::ReportId;
|
||||
pub use super::sessions::SessionId;
|
||||
pub use super::teams::TeamId;
|
||||
pub use super::threads::ThreadId;
|
||||
pub use super::threads::ThreadMessageId;
|
||||
pub use super::users::UserId;
|
||||
pub use crate::models::billing::{ChargeId, ProductId, ProductPriceId, UserSubscriptionId};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||
/// long when encoded as base62.
|
||||
///
|
||||
/// Uses `rand`'s thread rng on every call.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method panics if `n` is 0 or greater than 11, since a `u64`
|
||||
/// can only represent up to 11 character base62 strings
|
||||
#[inline]
|
||||
pub fn random_base62(n: usize) -> u64 {
|
||||
random_base62_rng(&mut rand::thread_rng(), n)
|
||||
}
|
||||
|
||||
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||
/// long when encoded as base62, using the given rng.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method panics if `n` is 0 or greater than 11, since a `u64`
|
||||
/// can only represent up to 11 character base62 strings
|
||||
pub fn random_base62_rng<R: rand::RngCore>(rng: &mut R, n: usize) -> u64 {
|
||||
random_base62_rng_range(rng, n, n)
|
||||
}
|
||||
|
||||
pub fn random_base62_rng_range<R: rand::RngCore>(rng: &mut R, n_min: usize, n_max: usize) -> u64 {
|
||||
use rand::Rng;
|
||||
assert!(n_min > 0 && n_max <= 11 && n_min <= n_max);
|
||||
// gen_range is [low, high): max value is `MULTIPLES[n] - 1`,
|
||||
// which is n characters long when encoded
|
||||
rng.gen_range(MULTIPLES[n_min - 1]..MULTIPLES[n_max])
|
||||
}
|
||||
|
||||
const MULTIPLES: [u64; 12] = [
|
||||
1,
|
||||
62,
|
||||
62 * 62,
|
||||
62 * 62 * 62,
|
||||
62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||
u64::MAX,
|
||||
];
|
||||
|
||||
/// An ID encoded as base62 for use in the API.
|
||||
///
|
||||
/// All ids should be random and encode to 8-10 character base62 strings,
|
||||
/// to avoid enumeration and other attacks.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Base62Id(pub u64);
|
||||
|
||||
/// An error decoding a number from base62.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DecodingError {
|
||||
/// Encountered a non-base62 character in a base62 string
|
||||
#[error("Invalid character {0:?} in base62 encoding")]
|
||||
InvalidBase62(char),
|
||||
/// Encountered integer overflow when decoding a base62 id.
|
||||
#[error("Base62 decoding overflowed")]
|
||||
Overflow,
|
||||
}
|
||||
|
||||
macro_rules! from_base62id {
|
||||
($($struct:ty, $con:expr;)+) => {
|
||||
$(
|
||||
impl From<Base62Id> for $struct {
|
||||
fn from(id: Base62Id) -> $struct {
|
||||
$con(id.0)
|
||||
}
|
||||
}
|
||||
impl From<$struct> for Base62Id {
|
||||
fn from(id: $struct) -> Base62Id {
|
||||
Base62Id(id.0)
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_base62_display {
|
||||
($struct:ty) => {
|
||||
impl std::fmt::Display for $struct {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&base62_impl::to_base62(self.0))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
impl_base62_display!(Base62Id);
|
||||
|
||||
macro_rules! base62_id_impl {
|
||||
($struct:ty, $cons:expr) => {
|
||||
from_base62id!($struct, $cons;);
|
||||
impl_base62_display!($struct);
|
||||
}
|
||||
}
|
||||
base62_id_impl!(ProjectId, ProjectId);
|
||||
base62_id_impl!(UserId, UserId);
|
||||
base62_id_impl!(VersionId, VersionId);
|
||||
base62_id_impl!(CollectionId, CollectionId);
|
||||
base62_id_impl!(TeamId, TeamId);
|
||||
base62_id_impl!(OrganizationId, OrganizationId);
|
||||
base62_id_impl!(ReportId, ReportId);
|
||||
base62_id_impl!(NotificationId, NotificationId);
|
||||
base62_id_impl!(ThreadId, ThreadId);
|
||||
base62_id_impl!(ThreadMessageId, ThreadMessageId);
|
||||
base62_id_impl!(SessionId, SessionId);
|
||||
base62_id_impl!(PatId, PatId);
|
||||
base62_id_impl!(ImageId, ImageId);
|
||||
base62_id_impl!(OAuthClientId, OAuthClientId);
|
||||
base62_id_impl!(OAuthRedirectUriId, OAuthRedirectUriId);
|
||||
base62_id_impl!(OAuthClientAuthorizationId, OAuthClientAuthorizationId);
|
||||
base62_id_impl!(PayoutId, PayoutId);
|
||||
base62_id_impl!(ProductId, ProductId);
|
||||
base62_id_impl!(ProductPriceId, ProductPriceId);
|
||||
base62_id_impl!(UserSubscriptionId, UserSubscriptionId);
|
||||
base62_id_impl!(ChargeId, ChargeId);
|
||||
|
||||
pub mod base62_impl {
|
||||
use serde::de::{self, Deserializer, Visitor};
|
||||
use serde::ser::Serializer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Base62Id, DecodingError};
|
||||
|
||||
impl<'de> Deserialize<'de> for Base62Id {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct Base62Visitor;
|
||||
|
||||
impl<'de> Visitor<'de> for Base62Visitor {
|
||||
type Value = Base62Id;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a base62 string id")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, string: &str) -> Result<Base62Id, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
parse_base62(string).map(Base62Id).map_err(E::custom)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(Base62Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Base62Id {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&to_base62(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
const BASE62_CHARS: [u8; 62] =
|
||||
*b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
pub fn to_base62(mut num: u64) -> String {
|
||||
let length = (num as f64).log(62.0).ceil() as usize;
|
||||
let mut output = String::with_capacity(length);
|
||||
|
||||
while num > 0 {
|
||||
// Could be done more efficiently, but requires byte
|
||||
// manipulation of strings & Vec<u8> -> String conversion
|
||||
output.insert(0, BASE62_CHARS[(num % 62) as usize] as char);
|
||||
num /= 62;
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
pub fn parse_base62(string: &str) -> Result<u64, DecodingError> {
|
||||
let mut num: u64 = 0;
|
||||
for c in string.chars() {
|
||||
let next_digit;
|
||||
if c.is_ascii_digit() {
|
||||
next_digit = (c as u8 - b'0') as u64;
|
||||
} else if c.is_ascii_uppercase() {
|
||||
next_digit = 10 + (c as u8 - b'A') as u64;
|
||||
} else if c.is_ascii_lowercase() {
|
||||
next_digit = 36 + (c as u8 - b'a') as u64;
|
||||
} else {
|
||||
return Err(DecodingError::InvalidBase62(c));
|
||||
}
|
||||
|
||||
// We don't want this panicking or wrapping on integer overflow
|
||||
if let Some(n) = num.checked_mul(62).and_then(|n| n.checked_add(next_digit)) {
|
||||
num = n;
|
||||
} else {
|
||||
return Err(DecodingError::Overflow);
|
||||
}
|
||||
}
|
||||
Ok(num)
|
||||
}
|
||||
}
|
||||
124
apps/labrinth/src/models/v3/images.rs
Normal file
124
apps/labrinth/src/models/v3/images.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use super::{
|
||||
ids::{Base62Id, ProjectId, ThreadMessageId, VersionId},
|
||||
pats::Scopes,
|
||||
reports::ReportId,
|
||||
users::UserId,
|
||||
};
|
||||
use crate::database::models::image_item::Image as DBImage;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct ImageId(pub u64);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Image {
|
||||
pub id: ImageId,
|
||||
pub url: String,
|
||||
pub size: u64,
|
||||
pub created: DateTime<Utc>,
|
||||
pub owner_id: UserId,
|
||||
|
||||
// context it is associated with
|
||||
#[serde(flatten)]
|
||||
pub context: ImageContext,
|
||||
}
|
||||
|
||||
impl From<DBImage> for Image {
|
||||
fn from(x: DBImage) -> Self {
|
||||
let mut context = ImageContext::from_str(&x.context, None);
|
||||
match &mut context {
|
||||
ImageContext::Project { project_id } => {
|
||||
*project_id = x.project_id.map(|x| x.into());
|
||||
}
|
||||
ImageContext::Version { version_id } => {
|
||||
*version_id = x.version_id.map(|x| x.into());
|
||||
}
|
||||
ImageContext::ThreadMessage { thread_message_id } => {
|
||||
*thread_message_id = x.thread_message_id.map(|x| x.into());
|
||||
}
|
||||
ImageContext::Report { report_id } => {
|
||||
*report_id = x.report_id.map(|x| x.into());
|
||||
}
|
||||
ImageContext::Unknown => {}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: x.id.into(),
|
||||
url: x.url,
|
||||
size: x.size,
|
||||
created: x.created,
|
||||
owner_id: x.owner_id.into(),
|
||||
context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "context")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ImageContext {
|
||||
Project {
|
||||
project_id: Option<ProjectId>,
|
||||
},
|
||||
Version {
|
||||
// version changelogs
|
||||
version_id: Option<VersionId>,
|
||||
},
|
||||
ThreadMessage {
|
||||
thread_message_id: Option<ThreadMessageId>,
|
||||
},
|
||||
Report {
|
||||
report_id: Option<ReportId>,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl ImageContext {
|
||||
pub fn context_as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ImageContext::Project { .. } => "project",
|
||||
ImageContext::Version { .. } => "version",
|
||||
ImageContext::ThreadMessage { .. } => "thread_message",
|
||||
ImageContext::Report { .. } => "report",
|
||||
ImageContext::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
pub fn inner_id(&self) -> Option<u64> {
|
||||
match self {
|
||||
ImageContext::Project { project_id } => project_id.map(|x| x.0),
|
||||
ImageContext::Version { version_id } => version_id.map(|x| x.0),
|
||||
ImageContext::ThreadMessage { thread_message_id } => thread_message_id.map(|x| x.0),
|
||||
ImageContext::Report { report_id } => report_id.map(|x| x.0),
|
||||
ImageContext::Unknown => None,
|
||||
}
|
||||
}
|
||||
pub fn relevant_scope(&self) -> Scopes {
|
||||
match self {
|
||||
ImageContext::Project { .. } => Scopes::PROJECT_WRITE,
|
||||
ImageContext::Version { .. } => Scopes::VERSION_WRITE,
|
||||
ImageContext::ThreadMessage { .. } => Scopes::THREAD_WRITE,
|
||||
ImageContext::Report { .. } => Scopes::REPORT_WRITE,
|
||||
ImageContext::Unknown => Scopes::NONE,
|
||||
}
|
||||
}
|
||||
pub fn from_str(context: &str, id: Option<u64>) -> Self {
|
||||
match context {
|
||||
"project" => ImageContext::Project {
|
||||
project_id: id.map(ProjectId),
|
||||
},
|
||||
"version" => ImageContext::Version {
|
||||
version_id: id.map(VersionId),
|
||||
},
|
||||
"thread_message" => ImageContext::ThreadMessage {
|
||||
thread_message_id: id.map(ThreadMessageId),
|
||||
},
|
||||
"report" => ImageContext::Report {
|
||||
report_id: id.map(ReportId),
|
||||
},
|
||||
_ => ImageContext::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
17
apps/labrinth/src/models/v3/mod.rs
Normal file
17
apps/labrinth/src/models/v3/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
pub mod analytics;
|
||||
pub mod billing;
|
||||
pub mod collections;
|
||||
pub mod ids;
|
||||
pub mod images;
|
||||
pub mod notifications;
|
||||
pub mod oauth_clients;
|
||||
pub mod organizations;
|
||||
pub mod pack;
|
||||
pub mod pats;
|
||||
pub mod payouts;
|
||||
pub mod projects;
|
||||
pub mod reports;
|
||||
pub mod sessions;
|
||||
pub mod teams;
|
||||
pub mod threads;
|
||||
pub mod users;
|
||||
216
apps/labrinth/src/models/v3/notifications.rs
Normal file
216
apps/labrinth/src/models/v3/notifications.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
use super::ids::Base62Id;
|
||||
use super::ids::OrganizationId;
|
||||
use super::users::UserId;
|
||||
use crate::database::models::notification_item::Notification as DBNotification;
|
||||
use crate::database::models::notification_item::NotificationAction as DBNotificationAction;
|
||||
use crate::models::ids::{ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId, VersionId};
|
||||
use crate::models::projects::ProjectStatus;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct NotificationId(pub u64);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Notification {
|
||||
pub id: NotificationId,
|
||||
pub user_id: UserId,
|
||||
pub read: bool,
|
||||
pub created: DateTime<Utc>,
|
||||
pub body: NotificationBody,
|
||||
|
||||
pub name: String,
|
||||
pub text: String,
|
||||
pub link: String,
|
||||
pub actions: Vec<NotificationAction>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum NotificationBody {
|
||||
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>,
|
||||
name: String,
|
||||
text: String,
|
||||
link: String,
|
||||
actions: Vec<NotificationAction>,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<DBNotification> for Notification {
|
||||
fn from(notif: DBNotification) -> Self {
|
||||
let (name, text, link, actions) = {
|
||||
match ¬if.body {
|
||||
NotificationBody::ProjectUpdate {
|
||||
project_id,
|
||||
version_id,
|
||||
} => (
|
||||
"A project you follow has been updated!".to_string(),
|
||||
format!(
|
||||
"The project {} has released a new version: {}",
|
||||
project_id, version_id
|
||||
),
|
||||
format!("/project/{}/version/{}", project_id, version_id),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::TeamInvite {
|
||||
project_id,
|
||||
role,
|
||||
team_id,
|
||||
..
|
||||
} => (
|
||||
"You have been invited to join a team!".to_string(),
|
||||
format!("An invite has been sent for you to be {} of a team", role),
|
||||
format!("/project/{}", project_id),
|
||||
vec![
|
||||
NotificationAction {
|
||||
name: "Accept".to_string(),
|
||||
action_route: ("POST".to_string(), format!("team/{team_id}/join")),
|
||||
},
|
||||
NotificationAction {
|
||||
name: "Deny".to_string(),
|
||||
action_route: (
|
||||
"DELETE".to_string(),
|
||||
format!("team/{team_id}/members/{}", UserId::from(notif.user_id)),
|
||||
),
|
||||
},
|
||||
],
|
||||
),
|
||||
NotificationBody::OrganizationInvite {
|
||||
organization_id,
|
||||
role,
|
||||
team_id,
|
||||
..
|
||||
} => (
|
||||
"You have been invited to join an organization!".to_string(),
|
||||
format!(
|
||||
"An invite has been sent for you to be {} of an organization",
|
||||
role
|
||||
),
|
||||
format!("/organization/{}", organization_id),
|
||||
vec![
|
||||
NotificationAction {
|
||||
name: "Accept".to_string(),
|
||||
action_route: ("POST".to_string(), format!("team/{team_id}/join")),
|
||||
},
|
||||
NotificationAction {
|
||||
name: "Deny".to_string(),
|
||||
action_route: (
|
||||
"DELETE".to_string(),
|
||||
format!(
|
||||
"organization/{organization_id}/members/{}",
|
||||
UserId::from(notif.user_id)
|
||||
),
|
||||
),
|
||||
},
|
||||
],
|
||||
),
|
||||
NotificationBody::StatusChange {
|
||||
old_status,
|
||||
new_status,
|
||||
project_id,
|
||||
} => (
|
||||
"Project status has changed".to_string(),
|
||||
format!(
|
||||
"Status has changed from {} to {}",
|
||||
old_status.as_friendly_str(),
|
||||
new_status.as_friendly_str()
|
||||
),
|
||||
format!("/project/{}", project_id),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::ModeratorMessage {
|
||||
project_id,
|
||||
report_id,
|
||||
..
|
||||
} => (
|
||||
"A moderator has sent you a message!".to_string(),
|
||||
"Click on the link to read more.".to_string(),
|
||||
if let Some(project_id) = project_id {
|
||||
format!("/project/{}", project_id)
|
||||
} else if let Some(report_id) = report_id {
|
||||
format!("/project/{}", report_id)
|
||||
} else {
|
||||
"#".to_string()
|
||||
},
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::LegacyMarkdown {
|
||||
name,
|
||||
text,
|
||||
link,
|
||||
actions,
|
||||
..
|
||||
} => (
|
||||
name.clone(),
|
||||
text.clone(),
|
||||
link.clone(),
|
||||
actions.clone().into_iter().map(Into::into).collect(),
|
||||
),
|
||||
NotificationBody::Unknown => {
|
||||
("".to_string(), "".to_string(), "#".to_string(), vec![])
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
id: notif.id.into(),
|
||||
user_id: notif.user_id.into(),
|
||||
body: notif.body,
|
||||
read: notif.read,
|
||||
created: notif.created,
|
||||
|
||||
name,
|
||||
text,
|
||||
link,
|
||||
actions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct NotificationAction {
|
||||
pub name: String,
|
||||
/// The route to call when this notification action is called. Formatted HTTP Method, route
|
||||
pub action_route: (String, String),
|
||||
}
|
||||
|
||||
impl From<DBNotificationAction> for NotificationAction {
|
||||
fn from(act: DBNotificationAction) -> Self {
|
||||
Self {
|
||||
name: act.name,
|
||||
action_route: (act.action_route_method, act.action_route),
|
||||
}
|
||||
}
|
||||
}
|
||||
125
apps/labrinth/src/models/v3/oauth_clients.rs
Normal file
125
apps/labrinth/src/models/v3/oauth_clients.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use super::{
|
||||
ids::{Base62Id, UserId},
|
||||
pats::Scopes,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
|
||||
use crate::database::models::oauth_client_authorization_item::OAuthClientAuthorization as DBOAuthClientAuthorization;
|
||||
use crate::database::models::oauth_client_item::OAuthClient as DBOAuthClient;
|
||||
use crate::database::models::oauth_client_item::OAuthRedirectUri as DBOAuthRedirectUri;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct OAuthClientId(pub u64);
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct OAuthClientAuthorizationId(pub u64);
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct OAuthRedirectUriId(pub u64);
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct OAuthRedirectUri {
|
||||
pub id: OAuthRedirectUriId,
|
||||
pub client_id: OAuthClientId,
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct OAuthClientCreationResult {
|
||||
#[serde(flatten)]
|
||||
pub client: OAuthClient,
|
||||
|
||||
pub client_secret: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct OAuthClient {
|
||||
pub id: OAuthClientId,
|
||||
pub name: String,
|
||||
pub icon_url: Option<String>,
|
||||
|
||||
// The maximum scopes the client can request for OAuth
|
||||
pub max_scopes: Scopes,
|
||||
|
||||
// The valid URIs that can be redirected to during an authorization request
|
||||
pub redirect_uris: Vec<OAuthRedirectUri>,
|
||||
|
||||
// The user that created (and thus controls) this client
|
||||
pub created_by: UserId,
|
||||
|
||||
// When this client was created
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
// (optional) Metadata about the client
|
||||
pub url: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct OAuthClientAuthorization {
|
||||
pub id: OAuthClientAuthorizationId,
|
||||
pub app_id: OAuthClientId,
|
||||
pub user_id: UserId,
|
||||
pub scopes: Scopes,
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct GetOAuthClientsRequest {
|
||||
#[serde_as(
|
||||
as = "serde_with::StringWithSeparator::<serde_with::formats::CommaSeparator, String>"
|
||||
)]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct DeleteOAuthClientQueryParam {
|
||||
pub client_id: OAuthClientId,
|
||||
}
|
||||
|
||||
impl From<DBOAuthClient> for OAuthClient {
|
||||
fn from(value: DBOAuthClient) -> Self {
|
||||
Self {
|
||||
id: value.id.into(),
|
||||
name: value.name,
|
||||
icon_url: value.icon_url,
|
||||
max_scopes: value.max_scopes,
|
||||
redirect_uris: value.redirect_uris.into_iter().map(|r| r.into()).collect(),
|
||||
created_by: value.created_by.into(),
|
||||
created: value.created,
|
||||
url: value.url,
|
||||
description: value.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DBOAuthRedirectUri> for OAuthRedirectUri {
|
||||
fn from(value: DBOAuthRedirectUri) -> Self {
|
||||
Self {
|
||||
id: value.id.into(),
|
||||
client_id: value.client_id.into(),
|
||||
uri: value.uri,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DBOAuthClientAuthorization> for OAuthClientAuthorization {
|
||||
fn from(value: DBOAuthClientAuthorization) -> Self {
|
||||
Self {
|
||||
id: value.id.into(),
|
||||
app_id: value.client_id.into(),
|
||||
user_id: value.user_id.into(),
|
||||
scopes: value.scopes,
|
||||
created: value.created,
|
||||
}
|
||||
}
|
||||
}
|
||||
52
apps/labrinth/src/models/v3/organizations.rs
Normal file
52
apps/labrinth/src/models/v3/organizations.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use super::{
|
||||
ids::{Base62Id, TeamId},
|
||||
teams::TeamMember,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The ID of a team
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Debug)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct OrganizationId(pub u64);
|
||||
|
||||
/// An organization of users who control a project
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Organization {
|
||||
/// The id of the organization
|
||||
pub id: OrganizationId,
|
||||
/// The slug of the organization
|
||||
pub slug: String,
|
||||
/// The title of the organization
|
||||
pub name: String,
|
||||
/// The associated team of the organization
|
||||
pub team_id: TeamId,
|
||||
/// The description of the organization
|
||||
pub description: String,
|
||||
|
||||
/// The icon url of the organization
|
||||
pub icon_url: Option<String>,
|
||||
/// The color of the organization (picked from the icon)
|
||||
pub color: Option<u32>,
|
||||
|
||||
/// A list of the members of the organization
|
||||
pub members: Vec<TeamMember>,
|
||||
}
|
||||
|
||||
impl Organization {
|
||||
pub fn from(
|
||||
data: crate::database::models::organization_item::Organization,
|
||||
team_members: Vec<TeamMember>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: data.id.into(),
|
||||
slug: data.slug,
|
||||
name: data.name,
|
||||
team_id: data.team_id.into(),
|
||||
description: data.description,
|
||||
members: team_members,
|
||||
icon_url: data.icon_url,
|
||||
color: data.color,
|
||||
}
|
||||
}
|
||||
}
|
||||
109
apps/labrinth/src/models/v3/pack.rs
Normal file
109
apps/labrinth/src/models/v3/pack.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use crate::{models::v2::projects::LegacySideType, util::env::parse_strings_from_var};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate, Eq, PartialEq, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PackFormat {
|
||||
pub game: String,
|
||||
pub format_version: i32,
|
||||
#[validate(length(min = 1, max = 512))]
|
||||
pub version_id: String,
|
||||
#[validate(length(min = 1, max = 512))]
|
||||
pub name: String,
|
||||
#[validate(length(max = 2048))]
|
||||
pub summary: Option<String>,
|
||||
#[validate]
|
||||
pub files: Vec<PackFile>,
|
||||
pub dependencies: std::collections::HashMap<PackDependency, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate, Eq, PartialEq, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PackFile {
|
||||
pub path: String,
|
||||
pub hashes: std::collections::HashMap<PackFileHash, String>,
|
||||
pub env: Option<std::collections::HashMap<EnvType, LegacySideType>>, // TODO: Should this use LegacySideType? Will probably require a overhaul of mrpack format to change this
|
||||
#[validate(custom(function = "validate_download_url"))]
|
||||
pub downloads: Vec<String>,
|
||||
pub file_size: u32,
|
||||
}
|
||||
|
||||
fn validate_download_url(values: &[String]) -> Result<(), validator::ValidationError> {
|
||||
for value in values {
|
||||
let url = url::Url::parse(value)
|
||||
.ok()
|
||||
.ok_or_else(|| validator::ValidationError::new("invalid URL"))?;
|
||||
|
||||
if url.as_str() != value {
|
||||
return Err(validator::ValidationError::new("invalid URL"));
|
||||
}
|
||||
|
||||
let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS").unwrap_or_default();
|
||||
if !domains.contains(
|
||||
&url.domain()
|
||||
.ok_or_else(|| validator::ValidationError::new("invalid URL"))?
|
||||
.to_string(),
|
||||
) {
|
||||
return Err(validator::ValidationError::new(
|
||||
"File download source is not from allowed sources",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase", from = "String")]
|
||||
pub enum PackFileHash {
|
||||
Sha1,
|
||||
Sha512,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<String> for PackFileHash {
|
||||
fn from(s: String) -> Self {
|
||||
return match s.as_str() {
|
||||
"sha1" => PackFileHash::Sha1,
|
||||
"sha512" => PackFileHash::Sha512,
|
||||
_ => PackFileHash::Unknown(s),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EnvType {
|
||||
Client,
|
||||
Server,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PackDependency {
|
||||
Forge,
|
||||
Neoforge,
|
||||
FabricLoader,
|
||||
QuiltLoader,
|
||||
Minecraft,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PackDependency {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl PackDependency {
|
||||
// These are constant, so this can remove unnecessary allocations (`to_string`)
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
PackDependency::Forge => "forge",
|
||||
PackDependency::Neoforge => "neoforge",
|
||||
PackDependency::FabricLoader => "fabric-loader",
|
||||
PackDependency::Minecraft => "minecraft",
|
||||
PackDependency::QuiltLoader => "quilt-loader",
|
||||
}
|
||||
}
|
||||
}
|
||||
241
apps/labrinth/src/models/v3/pats.rs
Normal file
241
apps/labrinth/src/models/v3/pats.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
use super::ids::Base62Id;
|
||||
use crate::bitflags_serde_impl;
|
||||
use crate::models::ids::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The ID of a team
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Debug)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct PatId(pub u64);
|
||||
|
||||
bitflags::bitflags! {
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Scopes: u64 {
|
||||
// read a user's email
|
||||
const USER_READ_EMAIL = 1 << 0;
|
||||
// read a user's data
|
||||
const USER_READ = 1 << 1;
|
||||
// write to a user's profile (edit username, email, avatar, follows, etc)
|
||||
const USER_WRITE = 1 << 2;
|
||||
// delete a user
|
||||
const USER_DELETE = 1 << 3;
|
||||
// modify a user's authentication data
|
||||
const USER_AUTH_WRITE = 1 << 4;
|
||||
|
||||
// read a user's notifications
|
||||
const NOTIFICATION_READ = 1 << 5;
|
||||
// delete or read a notification
|
||||
const NOTIFICATION_WRITE = 1 << 6;
|
||||
|
||||
// read a user's payouts data
|
||||
const PAYOUTS_READ = 1 << 7;
|
||||
// withdraw money from a user's account
|
||||
const PAYOUTS_WRITE = 1<< 8;
|
||||
// access user analytics (payout analytics at the moment)
|
||||
const ANALYTICS = 1 << 9;
|
||||
|
||||
// create a project
|
||||
const PROJECT_CREATE = 1 << 10;
|
||||
// read a user's projects (including private)
|
||||
const PROJECT_READ = 1 << 11;
|
||||
// write to a project's data (metadata, title, team members, etc)
|
||||
const PROJECT_WRITE = 1 << 12;
|
||||
// delete a project
|
||||
const PROJECT_DELETE = 1 << 13;
|
||||
|
||||
// create a version
|
||||
const VERSION_CREATE = 1 << 14;
|
||||
// read a user's versions (including private)
|
||||
const VERSION_READ = 1 << 15;
|
||||
// write to a version's data (metadata, files, etc)
|
||||
const VERSION_WRITE = 1 << 16;
|
||||
// delete a version
|
||||
const VERSION_DELETE = 1 << 17;
|
||||
|
||||
// create a report
|
||||
const REPORT_CREATE = 1 << 18;
|
||||
// read a user's reports
|
||||
const REPORT_READ = 1 << 19;
|
||||
// edit a report
|
||||
const REPORT_WRITE = 1 << 20;
|
||||
// delete a report
|
||||
const REPORT_DELETE = 1 << 21;
|
||||
|
||||
// read a thread
|
||||
const THREAD_READ = 1 << 22;
|
||||
// write to a thread (send a message, delete a message)
|
||||
const THREAD_WRITE = 1 << 23;
|
||||
|
||||
// create a pat
|
||||
const PAT_CREATE = 1 << 24;
|
||||
// read a user's pats
|
||||
const PAT_READ = 1 << 25;
|
||||
// edit a pat
|
||||
const PAT_WRITE = 1 << 26;
|
||||
// delete a pat
|
||||
const PAT_DELETE = 1 << 27;
|
||||
|
||||
// read a user's sessions
|
||||
const SESSION_READ = 1 << 28;
|
||||
// delete a session
|
||||
const SESSION_DELETE = 1 << 29;
|
||||
|
||||
// perform analytics action
|
||||
const PERFORM_ANALYTICS = 1 << 30;
|
||||
|
||||
// create a collection
|
||||
const COLLECTION_CREATE = 1 << 31;
|
||||
// read a user's collections
|
||||
const COLLECTION_READ = 1 << 32;
|
||||
// write to a collection
|
||||
const COLLECTION_WRITE = 1 << 33;
|
||||
// delete a collection
|
||||
const COLLECTION_DELETE = 1 << 34;
|
||||
|
||||
// create an organization
|
||||
const ORGANIZATION_CREATE = 1 << 35;
|
||||
// read a user's organizations
|
||||
const ORGANIZATION_READ = 1 << 36;
|
||||
// write to an organization
|
||||
const ORGANIZATION_WRITE = 1 << 37;
|
||||
// delete an organization
|
||||
const ORGANIZATION_DELETE = 1 << 38;
|
||||
|
||||
// only accessible by modrinth-issued sessions
|
||||
const SESSION_ACCESS = 1 << 39;
|
||||
|
||||
const NONE = 0b0;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags_serde_impl!(Scopes, u64);
|
||||
|
||||
impl Scopes {
|
||||
// these scopes cannot be specified in a personal access token
|
||||
pub fn restricted() -> Scopes {
|
||||
Scopes::PAT_CREATE
|
||||
| Scopes::PAT_READ
|
||||
| Scopes::PAT_WRITE
|
||||
| Scopes::PAT_DELETE
|
||||
| Scopes::SESSION_READ
|
||||
| Scopes::SESSION_DELETE
|
||||
| Scopes::SESSION_ACCESS
|
||||
| Scopes::USER_AUTH_WRITE
|
||||
| Scopes::USER_DELETE
|
||||
| Scopes::PERFORM_ANALYTICS
|
||||
}
|
||||
|
||||
pub fn is_restricted(&self) -> bool {
|
||||
self.intersects(Self::restricted())
|
||||
}
|
||||
|
||||
pub fn parse_from_oauth_scopes(scopes: &str) -> Result<Scopes, bitflags::parser::ParseError> {
|
||||
let scopes = scopes.replace(['+', ' '], "|").replace("%20", "|");
|
||||
bitflags::parser::from_str(&scopes)
|
||||
}
|
||||
|
||||
pub fn to_postgres(&self) -> i64 {
|
||||
self.bits() as i64
|
||||
}
|
||||
|
||||
pub fn from_postgres(value: i64) -> Self {
|
||||
Self::from_bits(value as u64).unwrap_or(Scopes::NONE)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PersonalAccessToken {
|
||||
pub id: PatId,
|
||||
pub name: String,
|
||||
pub access_token: Option<String>,
|
||||
pub scopes: Scopes,
|
||||
pub user_id: UserId,
|
||||
pub created: DateTime<Utc>,
|
||||
pub expires: DateTime<Utc>,
|
||||
pub last_used: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl PersonalAccessToken {
|
||||
pub fn from(
|
||||
data: crate::database::models::pat_item::PersonalAccessToken,
|
||||
include_token: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: data.id.into(),
|
||||
name: data.name,
|
||||
access_token: if include_token {
|
||||
Some(data.access_token)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
scopes: data.scopes,
|
||||
user_id: data.user_id.into(),
|
||||
created: data.created,
|
||||
expires: data.expires,
|
||||
last_used: data.last_used,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
|
||||
#[test]
|
||||
fn test_parse_from_oauth_scopes_well_formed() {
|
||||
let raw = "USER_READ_EMAIL SESSION_READ ORGANIZATION_CREATE";
|
||||
let expected = Scopes::USER_READ_EMAIL | Scopes::SESSION_READ | Scopes::ORGANIZATION_CREATE;
|
||||
|
||||
let parsed = Scopes::parse_from_oauth_scopes(raw).unwrap();
|
||||
|
||||
assert_same_flags(expected, parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_from_oauth_scopes_empty() {
|
||||
let raw = "";
|
||||
let expected = Scopes::empty();
|
||||
|
||||
let parsed = Scopes::parse_from_oauth_scopes(raw).unwrap();
|
||||
|
||||
assert_same_flags(expected, parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_from_oauth_scopes_invalid_scopes() {
|
||||
let raw = "notascope";
|
||||
|
||||
let parsed = Scopes::parse_from_oauth_scopes(raw);
|
||||
|
||||
assert!(parsed.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_from_oauth_scopes_invalid_separator() {
|
||||
let raw = "USER_READ_EMAIL & SESSION_READ";
|
||||
|
||||
let parsed = Scopes::parse_from_oauth_scopes(raw);
|
||||
|
||||
assert!(parsed.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_from_oauth_scopes_url_encoded() {
|
||||
let raw = urlencoding::encode("PAT_WRITE COLLECTION_DELETE").to_string();
|
||||
let expected = Scopes::PAT_WRITE | Scopes::COLLECTION_DELETE;
|
||||
|
||||
let parsed = Scopes::parse_from_oauth_scopes(&raw).unwrap();
|
||||
|
||||
assert_same_flags(expected, parsed);
|
||||
}
|
||||
|
||||
fn assert_same_flags(expected: Scopes, actual: Scopes) {
|
||||
assert_eq!(
|
||||
expected.iter_names().map(|(name, _)| name).collect_vec(),
|
||||
actual.iter_names().map(|(name, _)| name).collect_vec()
|
||||
);
|
||||
}
|
||||
}
|
||||
176
apps/labrinth/src/models/v3/payouts.rs
Normal file
176
apps/labrinth/src/models/v3/payouts.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use crate::models::ids::{Base62Id, UserId};
|
||||
use chrono::{DateTime, Utc};
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct PayoutId(pub u64);
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Payout {
|
||||
pub id: PayoutId,
|
||||
pub user_id: UserId,
|
||||
pub status: PayoutStatus,
|
||||
pub created: DateTime<Utc>,
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub amount: Decimal,
|
||||
|
||||
#[serde(with = "rust_decimal::serde::float_option")]
|
||||
pub fee: Option<Decimal>,
|
||||
pub method: Option<PayoutMethodType>,
|
||||
/// the address this payout was sent to: ex: email, paypal email, venmo handle
|
||||
pub method_address: Option<String>,
|
||||
pub platform_id: Option<String>,
|
||||
}
|
||||
|
||||
impl Payout {
|
||||
pub fn from(data: crate::database::models::payout_item::Payout) -> Self {
|
||||
Self {
|
||||
id: data.id.into(),
|
||||
user_id: data.user_id.into(),
|
||||
status: data.status,
|
||||
created: data.created,
|
||||
amount: data.amount,
|
||||
fee: data.fee,
|
||||
method: data.method,
|
||||
method_address: data.method_address,
|
||||
platform_id: data.platform_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PayoutMethodType {
|
||||
Venmo,
|
||||
PayPal,
|
||||
Tremendous,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PayoutMethodType {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl PayoutMethodType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
PayoutMethodType::Venmo => "venmo",
|
||||
PayoutMethodType::PayPal => "paypal",
|
||||
PayoutMethodType::Tremendous => "tremendous",
|
||||
PayoutMethodType::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> PayoutMethodType {
|
||||
match string {
|
||||
"venmo" => PayoutMethodType::Venmo,
|
||||
"paypal" => PayoutMethodType::PayPal,
|
||||
"tremendous" => PayoutMethodType::Tremendous,
|
||||
_ => PayoutMethodType::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PayoutStatus {
|
||||
Success,
|
||||
InTransit,
|
||||
Cancelled,
|
||||
Cancelling,
|
||||
Failed,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PayoutStatus {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl PayoutStatus {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
PayoutStatus::Success => "success",
|
||||
PayoutStatus::InTransit => "in-transit",
|
||||
PayoutStatus::Cancelled => "cancelled",
|
||||
PayoutStatus::Cancelling => "cancelling",
|
||||
PayoutStatus::Failed => "failed",
|
||||
PayoutStatus::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> PayoutStatus {
|
||||
match string {
|
||||
"success" => PayoutStatus::Success,
|
||||
"in-transit" => PayoutStatus::InTransit,
|
||||
"cancelled" => PayoutStatus::Cancelled,
|
||||
"cancelling" => PayoutStatus::Cancelling,
|
||||
"failed" => PayoutStatus::Failed,
|
||||
_ => PayoutStatus::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct PayoutMethod {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub type_: PayoutMethodType,
|
||||
pub name: String,
|
||||
pub supported_countries: Vec<String>,
|
||||
pub image_url: Option<String>,
|
||||
pub interval: PayoutInterval,
|
||||
pub fee: PayoutMethodFee,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct PayoutMethodFee {
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub percentage: Decimal,
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub min: Decimal,
|
||||
#[serde(with = "rust_decimal::serde::float_option")]
|
||||
pub max: Option<Decimal>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PayoutDecimal(pub Decimal);
|
||||
|
||||
impl Serialize for PayoutDecimal {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
rust_decimal::serde::float::serialize(&self.0, serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PayoutDecimal {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let decimal = rust_decimal::serde::float::deserialize(deserializer)?;
|
||||
Ok(PayoutDecimal(decimal))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PayoutInterval {
|
||||
Standard {
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
min: Decimal,
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
max: Decimal,
|
||||
},
|
||||
Fixed {
|
||||
values: Vec<PayoutDecimal>,
|
||||
},
|
||||
}
|
||||
963
apps/labrinth/src/models/v3/projects.rs
Normal file
963
apps/labrinth/src/models/v3/projects.rs
Normal file
@@ -0,0 +1,963 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use super::ids::{Base62Id, OrganizationId};
|
||||
use super::teams::TeamId;
|
||||
use super::users::UserId;
|
||||
use crate::database::models::loader_fields::VersionField;
|
||||
use crate::database::models::project_item::{LinkUrl, QueryProject};
|
||||
use crate::database::models::version_item::QueryVersion;
|
||||
use crate::models::threads::ThreadId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// The ID of a specific project, encoded as base62 for usage in the API
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct ProjectId(pub u64);
|
||||
|
||||
/// The ID of a specific version of a project
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Debug)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct VersionId(pub u64);
|
||||
|
||||
/// A project returned from the API
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Project {
|
||||
/// The ID of the project, encoded as a base62 string.
|
||||
pub id: ProjectId,
|
||||
/// The slug of a project, used for vanity URLs
|
||||
pub slug: Option<String>,
|
||||
/// The aggregated project typs of the versions of this project
|
||||
pub project_types: Vec<String>,
|
||||
/// The aggregated games of the versions of this project
|
||||
pub games: Vec<String>,
|
||||
/// The team of people that has ownership of this project.
|
||||
pub team_id: TeamId,
|
||||
/// The optional organization of people that have ownership of this project.
|
||||
pub organization: Option<OrganizationId>,
|
||||
/// The title or name of the project.
|
||||
pub name: String,
|
||||
/// A short description of the project.
|
||||
pub summary: String,
|
||||
/// A long form description of the project.
|
||||
pub description: String,
|
||||
|
||||
/// The date at which the project was first published.
|
||||
pub published: DateTime<Utc>,
|
||||
|
||||
/// The date at which the project was first published.
|
||||
pub updated: DateTime<Utc>,
|
||||
|
||||
/// The date at which the project was first approved.
|
||||
//pub approved: Option<DateTime<Utc>>,
|
||||
pub approved: Option<DateTime<Utc>>,
|
||||
/// The date at which the project entered the moderation queue
|
||||
pub queued: Option<DateTime<Utc>>,
|
||||
|
||||
/// The status of the project
|
||||
pub status: ProjectStatus,
|
||||
/// The requested status of this projct
|
||||
pub requested_status: Option<ProjectStatus>,
|
||||
|
||||
/// DEPRECATED: moved to threads system
|
||||
/// The rejection data of the project
|
||||
pub moderator_message: Option<ModeratorMessage>,
|
||||
|
||||
/// The license of this project
|
||||
pub license: License,
|
||||
|
||||
/// The total number of downloads the project has had.
|
||||
pub downloads: u32,
|
||||
/// The total number of followers this project has accumulated
|
||||
pub followers: u32,
|
||||
|
||||
/// A list of the categories that the project is in.
|
||||
pub categories: Vec<String>,
|
||||
|
||||
/// A list of the categories that the project is in.
|
||||
pub additional_categories: Vec<String>,
|
||||
/// A list of loaders this project supports
|
||||
pub loaders: Vec<String>,
|
||||
|
||||
/// A list of ids for versions of the project.
|
||||
pub versions: Vec<VersionId>,
|
||||
/// The URL of the icon of the project
|
||||
pub icon_url: Option<String>,
|
||||
|
||||
/// A collection of links to the project's various pages.
|
||||
pub link_urls: HashMap<String, Link>,
|
||||
|
||||
/// A string of URLs to visual content featuring the project
|
||||
pub gallery: Vec<GalleryItem>,
|
||||
|
||||
/// The color of the project (picked from icon)
|
||||
pub color: Option<u32>,
|
||||
|
||||
/// The thread of the moderation messages of the project
|
||||
pub thread_id: ThreadId,
|
||||
|
||||
/// The monetization status of this project
|
||||
pub monetization_status: MonetizationStatus,
|
||||
|
||||
/// Aggregated loader-fields across its myriad of versions
|
||||
#[serde(flatten)]
|
||||
pub fields: HashMap<String, Vec<serde_json::Value>>,
|
||||
}
|
||||
|
||||
fn remove_duplicates(values: Vec<serde_json::Value>) -> Vec<serde_json::Value> {
|
||||
let mut seen = HashSet::new();
|
||||
values
|
||||
.into_iter()
|
||||
.filter(|value| {
|
||||
// Convert the JSON value to a string for comparison
|
||||
let as_string = value.to_string();
|
||||
// Check if the string is already in the set
|
||||
seen.insert(as_string)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// This is a helper function to convert a list of VersionFields into a HashMap of field name to vecs of values
|
||||
// This allows for removal of duplicates
|
||||
pub fn from_duplicate_version_fields(
|
||||
version_fields: Vec<VersionField>,
|
||||
) -> HashMap<String, Vec<serde_json::Value>> {
|
||||
let mut fields: HashMap<String, Vec<serde_json::Value>> = HashMap::new();
|
||||
for vf in version_fields {
|
||||
// We use a string directly, so we can remove duplicates
|
||||
let serialized = if let Some(inner_array) = vf.value.serialize_internal().as_array() {
|
||||
inner_array.clone()
|
||||
} else {
|
||||
vec![vf.value.serialize_internal()]
|
||||
};
|
||||
|
||||
// Create array if doesnt exist, otherwise push, or if json is an array, extend
|
||||
if let Some(arr) = fields.get_mut(&vf.field_name) {
|
||||
arr.extend(serialized);
|
||||
} else {
|
||||
fields.insert(vf.field_name, serialized);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates by converting to string and back
|
||||
for (_, v) in fields.iter_mut() {
|
||||
*v = remove_duplicates(v.clone());
|
||||
}
|
||||
fields
|
||||
}
|
||||
|
||||
impl From<QueryProject> for Project {
|
||||
fn from(data: QueryProject) -> Self {
|
||||
let fields = from_duplicate_version_fields(data.aggregate_version_fields);
|
||||
let m = data.inner;
|
||||
Self {
|
||||
id: m.id.into(),
|
||||
slug: m.slug,
|
||||
project_types: data.project_types,
|
||||
games: data.games,
|
||||
team_id: m.team_id.into(),
|
||||
organization: m.organization_id.map(|i| i.into()),
|
||||
name: m.name,
|
||||
summary: m.summary,
|
||||
description: m.description,
|
||||
published: m.published,
|
||||
updated: m.updated,
|
||||
approved: m.approved,
|
||||
queued: m.queued,
|
||||
status: m.status,
|
||||
requested_status: m.requested_status,
|
||||
moderator_message: if let Some(message) = m.moderation_message {
|
||||
Some(ModeratorMessage {
|
||||
message,
|
||||
body: m.moderation_message_body,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
license: License {
|
||||
id: m.license.clone(),
|
||||
name: match spdx::Expression::parse(&m.license) {
|
||||
Ok(spdx_expr) => {
|
||||
let mut vec: Vec<&str> = Vec::new();
|
||||
for node in spdx_expr.iter() {
|
||||
if let spdx::expression::ExprNode::Req(req) = node {
|
||||
if let Some(id) = req.req.license.id() {
|
||||
vec.push(id.full_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
// spdx crate returns AND/OR operations in postfix order
|
||||
// and it would be a lot more effort to make it actually in order
|
||||
// so let's just ignore that and make them comma-separated
|
||||
vec.join(", ")
|
||||
}
|
||||
Err(_) => "".to_string(),
|
||||
},
|
||||
url: m.license_url,
|
||||
},
|
||||
downloads: m.downloads as u32,
|
||||
followers: m.follows as u32,
|
||||
categories: data.categories,
|
||||
additional_categories: data.additional_categories,
|
||||
loaders: m.loaders,
|
||||
versions: data.versions.into_iter().map(|v| v.into()).collect(),
|
||||
icon_url: m.icon_url,
|
||||
link_urls: data
|
||||
.urls
|
||||
.into_iter()
|
||||
.map(|d| (d.platform_name.clone(), Link::from(d)))
|
||||
.collect(),
|
||||
gallery: data
|
||||
.gallery_items
|
||||
.into_iter()
|
||||
.map(|x| GalleryItem {
|
||||
url: x.image_url,
|
||||
raw_url: x.raw_image_url,
|
||||
featured: x.featured,
|
||||
name: x.name,
|
||||
description: x.description,
|
||||
created: x.created,
|
||||
ordering: x.ordering,
|
||||
})
|
||||
.collect(),
|
||||
color: m.color,
|
||||
thread_id: data.thread_id.into(),
|
||||
monetization_status: m.monetization_status,
|
||||
fields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Project {
|
||||
// Matches the from QueryProject, but with a ResultSearchProject
|
||||
// pub fn from_search(m: ResultSearchProject) -> Option<Self> {
|
||||
// let project_id = ProjectId(parse_base62(&m.project_id).ok()?);
|
||||
// let team_id = TeamId(parse_base62(&m.team_id).ok()?);
|
||||
// let organization_id = m
|
||||
// .organization_id
|
||||
// .and_then(|id| Some(OrganizationId(parse_base62(&id).ok()?)));
|
||||
// let thread_id = ThreadId(parse_base62(&m.thread_id).ok()?);
|
||||
// let versions = m
|
||||
// .versions
|
||||
// .iter()
|
||||
// .filter_map(|id| Some(VersionId(parse_base62(id).ok()?)))
|
||||
// .collect();
|
||||
//
|
||||
// let approved = DateTime::parse_from_rfc3339(&m.date_created).ok()?;
|
||||
// let published = DateTime::parse_from_rfc3339(&m.date_published).ok()?.into();
|
||||
// let approved = if approved == published {
|
||||
// None
|
||||
// } else {
|
||||
// Some(approved.into())
|
||||
// };
|
||||
//
|
||||
// let updated = DateTime::parse_from_rfc3339(&m.date_modified).ok()?.into();
|
||||
// let queued = m
|
||||
// .date_queued
|
||||
// .and_then(|dq| DateTime::parse_from_rfc3339(&dq).ok())
|
||||
// .map(|d| d.into());
|
||||
//
|
||||
// let status = ProjectStatus::from_string(&m.status);
|
||||
// let requested_status = m
|
||||
// .requested_status
|
||||
// .map(|mrs| ProjectStatus::from_string(&mrs));
|
||||
//
|
||||
// let license_url = m.license_url;
|
||||
// let icon_url = m.icon_url;
|
||||
//
|
||||
// // Loaders
|
||||
// let mut loaders = m.loaders;
|
||||
// let mrpack_loaders_strings =
|
||||
// m.project_loader_fields
|
||||
// .get("mrpack_loaders")
|
||||
// .cloned()
|
||||
// .map(|v| {
|
||||
// v.into_iter()
|
||||
// .filter_map(|v| v.as_str().map(String::from))
|
||||
// .collect_vec()
|
||||
// });
|
||||
//
|
||||
// // If the project has a mrpack loader, keep only 'loaders' that are not in the mrpack_loaders
|
||||
// if let Some(ref mrpack_loaders) = mrpack_loaders_strings {
|
||||
// loaders.retain(|l| !mrpack_loaders.contains(l));
|
||||
// }
|
||||
//
|
||||
// // Categories
|
||||
// let mut categories = m.display_categories.clone();
|
||||
// categories.retain(|c| !loaders.contains(c));
|
||||
// if let Some(ref mrpack_loaders) = mrpack_loaders_strings {
|
||||
// categories.retain(|l| !mrpack_loaders.contains(l));
|
||||
// }
|
||||
//
|
||||
// // Additional categories
|
||||
// let mut additional_categories = m.categories.clone();
|
||||
// additional_categories.retain(|c| !categories.contains(c));
|
||||
// additional_categories.retain(|c| !loaders.contains(c));
|
||||
// if let Some(ref mrpack_loaders) = mrpack_loaders_strings {
|
||||
// additional_categories.retain(|l| !mrpack_loaders.contains(l));
|
||||
// }
|
||||
//
|
||||
// let games = m.games;
|
||||
//
|
||||
// let monetization_status = m
|
||||
// .monetization_status
|
||||
// .as_deref()
|
||||
// .map(MonetizationStatus::from_string)
|
||||
// .unwrap_or(MonetizationStatus::Monetized);
|
||||
//
|
||||
// let link_urls = m
|
||||
// .links
|
||||
// .into_iter()
|
||||
// .map(|d| (d.platform_name.clone(), Link::from(d)))
|
||||
// .collect();
|
||||
//
|
||||
// let gallery = m
|
||||
// .gallery_items
|
||||
// .into_iter()
|
||||
// .map(|x| GalleryItem {
|
||||
// url: x.image_url,
|
||||
// featured: x.featured,
|
||||
// name: x.name,
|
||||
// description: x.description,
|
||||
// created: x.created,
|
||||
// ordering: x.ordering,
|
||||
// })
|
||||
// .collect();
|
||||
//
|
||||
// Some(Self {
|
||||
// id: project_id,
|
||||
// slug: m.slug,
|
||||
// project_types: m.project_types,
|
||||
// games,
|
||||
// team_id,
|
||||
// organization: organization_id,
|
||||
// name: m.name,
|
||||
// summary: m.summary,
|
||||
// description: "".to_string(), // Body is potentially huge, do not store in search
|
||||
// published,
|
||||
// updated,
|
||||
// approved,
|
||||
// queued,
|
||||
// status,
|
||||
// requested_status,
|
||||
// moderator_message: None, // Deprecated
|
||||
// license: License {
|
||||
// id: m.license.clone(),
|
||||
// name: match spdx::Expression::parse(&m.license) {
|
||||
// Ok(spdx_expr) => {
|
||||
// let mut vec: Vec<&str> = Vec::new();
|
||||
// for node in spdx_expr.iter() {
|
||||
// if let spdx::expression::ExprNode::Req(req) = node {
|
||||
// if let Some(id) = req.req.license.id() {
|
||||
// vec.push(id.full_name);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// // spdx crate returns AND/OR operations in postfix order
|
||||
// // and it would be a lot more effort to make it actually in order
|
||||
// // so let's just ignore that and make them comma-separated
|
||||
// vec.join(", ")
|
||||
// }
|
||||
// Err(_) => "".to_string(),
|
||||
// },
|
||||
// url: license_url,
|
||||
// },
|
||||
// downloads: m.downloads as u32,
|
||||
// followers: m.follows as u32,
|
||||
// categories,
|
||||
// additional_categories,
|
||||
// loaders,
|
||||
// versions,
|
||||
// icon_url,
|
||||
// link_urls,
|
||||
// gallery,
|
||||
// color: m.color,
|
||||
// thread_id,
|
||||
// monetization_status,
|
||||
// fields: m
|
||||
// .project_loader_fields
|
||||
// .into_iter()
|
||||
// .map(|(k, v)| (k, v.into_iter().collect()))
|
||||
// .collect(),
|
||||
// })
|
||||
// }
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct GalleryItem {
|
||||
pub url: String,
|
||||
pub raw_url: String,
|
||||
pub featured: bool,
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub created: DateTime<Utc>,
|
||||
pub ordering: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ModeratorMessage {
|
||||
pub message: String,
|
||||
pub body: Option<String>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_LICENSE_ID: &str = "LicenseRef-All-Rights-Reserved";
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct License {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate, Clone, Eq, PartialEq)]
|
||||
pub struct Link {
|
||||
pub platform: String,
|
||||
pub donation: bool,
|
||||
#[validate(
|
||||
custom(function = "crate::util::validate::validate_url"),
|
||||
length(max = 2048)
|
||||
)]
|
||||
pub url: String,
|
||||
}
|
||||
impl From<LinkUrl> for Link {
|
||||
fn from(data: LinkUrl) -> Self {
|
||||
Self {
|
||||
platform: data.platform_name,
|
||||
donation: data.donation,
|
||||
url: data.url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A status decides the visibility of a project in search, URLs, and the whole site itself.
|
||||
/// Approved - Project is displayed on search, and accessible by URL
|
||||
/// Rejected - Project is not displayed on search, and not accessible by URL (Temporary state, project can reapply)
|
||||
/// Draft - Project is not displayed on search, and not accessible by URL
|
||||
/// Unlisted - Project is not displayed on search, but accessible by URL
|
||||
/// Withheld - Same as unlisted, but set by a moderator. Cannot be switched to another type without moderator approval
|
||||
/// Processing - Project is not displayed on search, and not accessible by URL (Temporary state, project under review)
|
||||
/// Scheduled - Project is scheduled to be released in the future
|
||||
/// Private - Project is approved, but is not viewable to the public
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProjectStatus {
|
||||
Approved,
|
||||
Archived,
|
||||
Rejected,
|
||||
Draft,
|
||||
Unlisted,
|
||||
Processing,
|
||||
Withheld,
|
||||
Scheduled,
|
||||
Private,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ProjectStatus {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectStatus {
|
||||
pub fn from_string(string: &str) -> ProjectStatus {
|
||||
match string {
|
||||
"processing" => ProjectStatus::Processing,
|
||||
"rejected" => ProjectStatus::Rejected,
|
||||
"approved" => ProjectStatus::Approved,
|
||||
"draft" => ProjectStatus::Draft,
|
||||
"unlisted" => ProjectStatus::Unlisted,
|
||||
"archived" => ProjectStatus::Archived,
|
||||
"withheld" => ProjectStatus::Withheld,
|
||||
"private" => ProjectStatus::Private,
|
||||
_ => ProjectStatus::Unknown,
|
||||
}
|
||||
}
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ProjectStatus::Approved => "approved",
|
||||
ProjectStatus::Rejected => "rejected",
|
||||
ProjectStatus::Draft => "draft",
|
||||
ProjectStatus::Unlisted => "unlisted",
|
||||
ProjectStatus::Processing => "processing",
|
||||
ProjectStatus::Unknown => "unknown",
|
||||
ProjectStatus::Archived => "archived",
|
||||
ProjectStatus::Withheld => "withheld",
|
||||
ProjectStatus::Scheduled => "scheduled",
|
||||
ProjectStatus::Private => "private",
|
||||
}
|
||||
}
|
||||
pub fn as_friendly_str(&self) -> &'static str {
|
||||
match self {
|
||||
ProjectStatus::Approved => "Listed",
|
||||
ProjectStatus::Rejected => "Rejected",
|
||||
ProjectStatus::Draft => "Draft",
|
||||
ProjectStatus::Unlisted => "Unlisted",
|
||||
ProjectStatus::Processing => "Under review",
|
||||
ProjectStatus::Unknown => "Unknown",
|
||||
ProjectStatus::Archived => "Archived",
|
||||
ProjectStatus::Withheld => "Withheld",
|
||||
ProjectStatus::Scheduled => "Scheduled",
|
||||
ProjectStatus::Private => "Private",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iterator() -> impl Iterator<Item = ProjectStatus> {
|
||||
[
|
||||
ProjectStatus::Approved,
|
||||
ProjectStatus::Archived,
|
||||
ProjectStatus::Rejected,
|
||||
ProjectStatus::Draft,
|
||||
ProjectStatus::Unlisted,
|
||||
ProjectStatus::Processing,
|
||||
ProjectStatus::Withheld,
|
||||
ProjectStatus::Scheduled,
|
||||
ProjectStatus::Private,
|
||||
ProjectStatus::Unknown,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
|
||||
// Project pages + info cannot be viewed
|
||||
pub fn is_hidden(&self) -> bool {
|
||||
match self {
|
||||
ProjectStatus::Rejected => true,
|
||||
ProjectStatus::Draft => true,
|
||||
ProjectStatus::Processing => true,
|
||||
ProjectStatus::Unknown => true,
|
||||
ProjectStatus::Scheduled => true,
|
||||
ProjectStatus::Private => true,
|
||||
|
||||
ProjectStatus::Approved => false,
|
||||
ProjectStatus::Unlisted => false,
|
||||
ProjectStatus::Archived => false,
|
||||
ProjectStatus::Withheld => false,
|
||||
}
|
||||
}
|
||||
|
||||
// Project can be displayed in search
|
||||
pub fn is_searchable(&self) -> bool {
|
||||
matches!(self, ProjectStatus::Approved | ProjectStatus::Archived)
|
||||
}
|
||||
|
||||
// Project is "Approved" by moderators
|
||||
pub fn is_approved(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ProjectStatus::Approved
|
||||
| ProjectStatus::Archived
|
||||
| ProjectStatus::Unlisted
|
||||
| ProjectStatus::Private
|
||||
)
|
||||
}
|
||||
|
||||
// Project status can be requested after moderator approval
|
||||
pub fn can_be_requested(&self) -> bool {
|
||||
match self {
|
||||
ProjectStatus::Approved => true,
|
||||
ProjectStatus::Archived => true,
|
||||
ProjectStatus::Unlisted => true,
|
||||
ProjectStatus::Private => true,
|
||||
ProjectStatus::Draft => true,
|
||||
|
||||
ProjectStatus::Rejected => false,
|
||||
ProjectStatus::Processing => false,
|
||||
ProjectStatus::Unknown => false,
|
||||
ProjectStatus::Withheld => false,
|
||||
ProjectStatus::Scheduled => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum MonetizationStatus {
|
||||
ForceDemonetized,
|
||||
Demonetized,
|
||||
Monetized,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MonetizationStatus {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl MonetizationStatus {
|
||||
pub fn from_string(string: &str) -> MonetizationStatus {
|
||||
match string {
|
||||
"force-demonetized" => MonetizationStatus::ForceDemonetized,
|
||||
"demonetized" => MonetizationStatus::Demonetized,
|
||||
"monetized" => MonetizationStatus::Monetized,
|
||||
_ => MonetizationStatus::Monetized,
|
||||
}
|
||||
}
|
||||
// These are constant, so this can remove unnecessary allocations (`to_string`)
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
MonetizationStatus::ForceDemonetized => "force-demonetized",
|
||||
MonetizationStatus::Demonetized => "demonetized",
|
||||
MonetizationStatus::Monetized => "monetized",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A specific version of a project
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Version {
|
||||
/// The ID of the version, encoded as a base62 string.
|
||||
pub id: VersionId,
|
||||
/// The ID of the project this version is for.
|
||||
pub project_id: ProjectId,
|
||||
/// The ID of the author who published this version
|
||||
pub author_id: UserId,
|
||||
/// Whether the version is featured or not
|
||||
pub featured: bool,
|
||||
/// The name of this version
|
||||
pub name: String,
|
||||
/// The version number. Ideally will follow semantic versioning
|
||||
pub version_number: String,
|
||||
/// Project types for which this version is compatible with, extracted from Loader
|
||||
pub project_types: Vec<String>,
|
||||
/// Games for which this version is compatible with, extracted from Loader/Project types
|
||||
pub games: Vec<String>,
|
||||
/// The changelog for this version of the project.
|
||||
pub changelog: String,
|
||||
|
||||
/// The date that this version was published.
|
||||
pub date_published: DateTime<Utc>,
|
||||
/// The number of downloads this specific version has had.
|
||||
pub downloads: u32,
|
||||
/// The type of the release - `Alpha`, `Beta`, or `Release`.
|
||||
pub version_type: VersionType,
|
||||
/// The status of tne version
|
||||
pub status: VersionStatus,
|
||||
/// The requested status of the version (used for scheduling)
|
||||
pub requested_status: Option<VersionStatus>,
|
||||
|
||||
/// A list of files available for download for this version.
|
||||
pub files: Vec<VersionFile>,
|
||||
/// A list of projects that this version depends on.
|
||||
pub dependencies: Vec<Dependency>,
|
||||
|
||||
/// The loaders that this version works on
|
||||
pub loaders: Vec<Loader>,
|
||||
/// Ordering override, lower is returned first
|
||||
pub ordering: Option<i32>,
|
||||
|
||||
// All other fields are loader-specific VersionFields
|
||||
// These are flattened during serialization
|
||||
#[serde(deserialize_with = "skip_nulls")]
|
||||
#[serde(flatten)]
|
||||
pub fields: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
pub fn skip_nulls<'de, D>(deserializer: D) -> Result<HashMap<String, serde_json::Value>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let mut map = HashMap::deserialize(deserializer)?;
|
||||
map.retain(|_, v: &mut serde_json::Value| !v.is_null());
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
impl From<QueryVersion> for Version {
|
||||
fn from(data: QueryVersion) -> Version {
|
||||
let v = data.inner;
|
||||
Version {
|
||||
id: v.id.into(),
|
||||
project_id: v.project_id.into(),
|
||||
author_id: v.author_id.into(),
|
||||
featured: v.featured,
|
||||
name: v.name,
|
||||
version_number: v.version_number,
|
||||
project_types: data.project_types,
|
||||
games: data.games,
|
||||
changelog: v.changelog,
|
||||
date_published: v.date_published,
|
||||
downloads: v.downloads as u32,
|
||||
version_type: match v.version_type.as_str() {
|
||||
"release" => VersionType::Release,
|
||||
"beta" => VersionType::Beta,
|
||||
"alpha" => VersionType::Alpha,
|
||||
_ => VersionType::Release,
|
||||
},
|
||||
ordering: v.ordering,
|
||||
|
||||
status: v.status,
|
||||
requested_status: v.requested_status,
|
||||
files: data
|
||||
.files
|
||||
.into_iter()
|
||||
.map(|f| VersionFile {
|
||||
url: f.url,
|
||||
filename: f.filename,
|
||||
hashes: f.hashes,
|
||||
primary: f.primary,
|
||||
size: f.size,
|
||||
file_type: f.file_type,
|
||||
})
|
||||
.collect(),
|
||||
dependencies: data
|
||||
.dependencies
|
||||
.into_iter()
|
||||
.map(|d| Dependency {
|
||||
version_id: d.version_id.map(|i| VersionId(i.0 as u64)),
|
||||
project_id: d.project_id.map(|i| ProjectId(i.0 as u64)),
|
||||
file_name: d.file_name,
|
||||
dependency_type: DependencyType::from_string(d.dependency_type.as_str()),
|
||||
})
|
||||
.collect(),
|
||||
loaders: data.loaders.into_iter().map(Loader).collect(),
|
||||
// Only add the internal component of the field for display
|
||||
// "ie": "game_versions",["1.2.3"] instead of "game_versions",ArrayEnum(...)
|
||||
fields: data
|
||||
.version_fields
|
||||
.into_iter()
|
||||
.map(|vf| (vf.field_name, vf.value.serialize_internal()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A status decides the visibility of a project in search, URLs, and the whole site itself.
|
||||
/// Listed - Version is displayed on project, and accessible by URL
|
||||
/// Archived - Identical to listed but has a message displayed stating version is unsupported
|
||||
/// Draft - Version is not displayed on project, and not accessible by URL
|
||||
/// Unlisted - Version is not displayed on project, and accessible by URL
|
||||
/// Scheduled - Version is scheduled to be released in the future
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VersionStatus {
|
||||
Listed,
|
||||
Archived,
|
||||
Draft,
|
||||
Unlisted,
|
||||
Scheduled,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VersionStatus {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl VersionStatus {
|
||||
pub fn from_string(string: &str) -> VersionStatus {
|
||||
match string {
|
||||
"listed" => VersionStatus::Listed,
|
||||
"draft" => VersionStatus::Draft,
|
||||
"unlisted" => VersionStatus::Unlisted,
|
||||
"scheduled" => VersionStatus::Scheduled,
|
||||
_ => VersionStatus::Unknown,
|
||||
}
|
||||
}
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
VersionStatus::Listed => "listed",
|
||||
VersionStatus::Archived => "archived",
|
||||
VersionStatus::Draft => "draft",
|
||||
VersionStatus::Unlisted => "unlisted",
|
||||
VersionStatus::Unknown => "unknown",
|
||||
VersionStatus::Scheduled => "scheduled",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iterator() -> impl Iterator<Item = VersionStatus> {
|
||||
[
|
||||
VersionStatus::Listed,
|
||||
VersionStatus::Archived,
|
||||
VersionStatus::Draft,
|
||||
VersionStatus::Unlisted,
|
||||
VersionStatus::Scheduled,
|
||||
VersionStatus::Unknown,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
|
||||
// Version pages + info cannot be viewed
|
||||
pub fn is_hidden(&self) -> bool {
|
||||
match self {
|
||||
VersionStatus::Listed => false,
|
||||
VersionStatus::Archived => false,
|
||||
VersionStatus::Unlisted => false,
|
||||
|
||||
VersionStatus::Draft => true,
|
||||
VersionStatus::Scheduled => true,
|
||||
VersionStatus::Unknown => true,
|
||||
}
|
||||
}
|
||||
|
||||
// Whether version is listed on project / returned in aggregate routes
|
||||
pub fn is_listed(&self) -> bool {
|
||||
matches!(self, VersionStatus::Listed | VersionStatus::Archived)
|
||||
}
|
||||
|
||||
// Whether a version status can be requested
|
||||
pub fn can_be_requested(&self) -> bool {
|
||||
match self {
|
||||
VersionStatus::Listed => true,
|
||||
VersionStatus::Archived => true,
|
||||
VersionStatus::Draft => true,
|
||||
VersionStatus::Unlisted => true,
|
||||
VersionStatus::Scheduled => false,
|
||||
|
||||
VersionStatus::Unknown => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single project file, with a url for the file and the file's hash
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct VersionFile {
|
||||
/// A map of hashes of the file. The key is the hashing algorithm
|
||||
/// and the value is the string version of the hash.
|
||||
pub hashes: std::collections::HashMap<String, String>,
|
||||
/// A direct link to the file for downloading it.
|
||||
pub url: String,
|
||||
/// The filename of the file.
|
||||
pub filename: String,
|
||||
/// Whether the file is the primary file of a version
|
||||
pub primary: bool,
|
||||
/// The size in bytes of the file
|
||||
pub size: u32,
|
||||
/// The type of the file
|
||||
pub file_type: Option<FileType>,
|
||||
}
|
||||
|
||||
/// A dendency which describes what versions are required, break support, or are optional to the
|
||||
/// version's functionality
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Dependency {
|
||||
/// The specific version id that the dependency uses
|
||||
pub version_id: Option<VersionId>,
|
||||
/// The project ID that the dependency is synced with and auto-updated
|
||||
pub project_id: Option<ProjectId>,
|
||||
/// The filename of the dependency. Used exclusively for external mods on modpacks
|
||||
pub file_name: Option<String>,
|
||||
/// The type of the dependency
|
||||
pub dependency_type: DependencyType,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VersionType {
|
||||
Release,
|
||||
Beta,
|
||||
Alpha,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VersionType {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl VersionType {
|
||||
// These are constant, so this can remove unneccessary allocations (`to_string`)
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
VersionType::Release => "release",
|
||||
VersionType::Beta => "beta",
|
||||
VersionType::Alpha => "alpha",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DependencyType {
|
||||
Required,
|
||||
Optional,
|
||||
Incompatible,
|
||||
Embedded,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DependencyType {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl DependencyType {
|
||||
// These are constant, so this can remove unneccessary allocations (`to_string`)
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
DependencyType::Required => "required",
|
||||
DependencyType::Optional => "optional",
|
||||
DependencyType::Incompatible => "incompatible",
|
||||
DependencyType::Embedded => "embedded",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> DependencyType {
|
||||
match string {
|
||||
"required" => DependencyType::Required,
|
||||
"optional" => DependencyType::Optional,
|
||||
"incompatible" => DependencyType::Incompatible,
|
||||
"embedded" => DependencyType::Embedded,
|
||||
_ => DependencyType::Required,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum FileType {
|
||||
RequiredResourcePack,
|
||||
OptionalResourcePack,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FileType {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
// These are constant, so this can remove unnecessary allocations (`to_string`)
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
FileType::RequiredResourcePack => "required-resource-pack",
|
||||
FileType::OptionalResourcePack => "optional-resource-pack",
|
||||
FileType::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> FileType {
|
||||
match string {
|
||||
"required-resource-pack" => FileType::RequiredResourcePack,
|
||||
"optional-resource-pack" => FileType::OptionalResourcePack,
|
||||
"unknown" => FileType::Unknown,
|
||||
_ => FileType::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A project loader
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[serde(transparent)]
|
||||
pub struct Loader(pub String);
|
||||
|
||||
// These fields must always succeed parsing; deserialize errors aren't
|
||||
// processed correctly (don't return JSON errors)
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SearchRequest {
|
||||
pub query: Option<String>,
|
||||
pub offset: Option<String>,
|
||||
pub index: Option<String>,
|
||||
pub limit: Option<String>,
|
||||
|
||||
pub new_filters: Option<String>,
|
||||
|
||||
// TODO: Deprecated values below. WILL BE REMOVED V3!
|
||||
pub facets: Option<String>,
|
||||
pub filters: Option<String>,
|
||||
pub version: Option<String>,
|
||||
}
|
||||
73
apps/labrinth/src/models/v3/reports.rs
Normal file
73
apps/labrinth/src/models/v3/reports.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use super::ids::Base62Id;
|
||||
use crate::database::models::report_item::QueryReport as DBReport;
|
||||
use crate::models::ids::{ProjectId, ThreadId, UserId, VersionId};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct ReportId(pub u64);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Report {
|
||||
pub id: ReportId,
|
||||
pub report_type: String,
|
||||
pub item_id: String,
|
||||
pub item_type: ItemType,
|
||||
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 ItemType {
|
||||
Project,
|
||||
Version,
|
||||
User,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl ItemType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ItemType::Project => "project",
|
||||
ItemType::Version => "version",
|
||||
ItemType::User => "user",
|
||||
ItemType::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DBReport> for Report {
|
||||
fn from(x: DBReport) -> Self {
|
||||
let mut item_id = "".to_string();
|
||||
let mut item_type = ItemType::Unknown;
|
||||
|
||||
if let Some(project_id) = x.project_id {
|
||||
item_id = ProjectId::from(project_id).to_string();
|
||||
item_type = ItemType::Project;
|
||||
} else if let Some(version_id) = x.version_id {
|
||||
item_id = VersionId::from(version_id).to_string();
|
||||
item_type = ItemType::Version;
|
||||
} else if let Some(user_id) = x.user_id {
|
||||
item_id = UserId::from(user_id).to_string();
|
||||
item_type = ItemType::User;
|
||||
}
|
||||
|
||||
Report {
|
||||
id: x.id.into(),
|
||||
report_type: x.report_type,
|
||||
item_id,
|
||||
item_type,
|
||||
reporter: x.reporter.into(),
|
||||
body: x.body,
|
||||
created: x.created,
|
||||
closed: x.closed,
|
||||
thread_id: x.thread_id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
60
apps/labrinth/src/models/v3/sessions.rs
Normal file
60
apps/labrinth/src/models/v3/sessions.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use super::ids::Base62Id;
|
||||
use crate::models::users::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Debug)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct SessionId(pub u64);
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Session {
|
||||
pub id: SessionId,
|
||||
pub session: Option<String>,
|
||||
pub user_id: UserId,
|
||||
|
||||
pub created: DateTime<Utc>,
|
||||
pub last_login: DateTime<Utc>,
|
||||
pub expires: DateTime<Utc>,
|
||||
pub refresh_expires: DateTime<Utc>,
|
||||
|
||||
pub os: Option<String>,
|
||||
pub platform: Option<String>,
|
||||
pub user_agent: String,
|
||||
|
||||
pub city: Option<String>,
|
||||
pub country: Option<String>,
|
||||
pub ip: String,
|
||||
|
||||
pub current: bool,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn from(
|
||||
data: crate::database::models::session_item::Session,
|
||||
include_session: bool,
|
||||
current_session: Option<&str>,
|
||||
) -> Self {
|
||||
Session {
|
||||
id: data.id.into(),
|
||||
current: Some(&*data.session) == current_session,
|
||||
session: if include_session {
|
||||
Some(data.session)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
user_id: data.user_id.into(),
|
||||
created: data.created,
|
||||
last_login: data.last_login,
|
||||
expires: data.expires,
|
||||
refresh_expires: data.refresh_expires,
|
||||
os: data.os,
|
||||
platform: data.platform,
|
||||
user_agent: data.user_agent,
|
||||
city: data.city,
|
||||
country: data.country,
|
||||
ip: data.ip,
|
||||
}
|
||||
}
|
||||
}
|
||||
202
apps/labrinth/src/models/v3/teams.rs
Normal file
202
apps/labrinth/src/models/v3/teams.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use super::ids::Base62Id;
|
||||
use crate::bitflags_serde_impl;
|
||||
use crate::models::users::User;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The ID of a team
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct TeamId(pub u64);
|
||||
|
||||
pub const DEFAULT_ROLE: &str = "Member";
|
||||
|
||||
/// A team of users who control a project
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Team {
|
||||
/// The id of the team
|
||||
pub id: TeamId,
|
||||
/// A list of the members of the team
|
||||
pub members: Vec<TeamMember>,
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ProjectPermissions: u64 {
|
||||
const UPLOAD_VERSION = 1 << 0;
|
||||
const DELETE_VERSION = 1 << 1;
|
||||
const EDIT_DETAILS = 1 << 2;
|
||||
const EDIT_BODY = 1 << 3;
|
||||
const MANAGE_INVITES = 1 << 4;
|
||||
const REMOVE_MEMBER = 1 << 5;
|
||||
const EDIT_MEMBER = 1 << 6;
|
||||
const DELETE_PROJECT = 1 << 7;
|
||||
const VIEW_ANALYTICS = 1 << 8;
|
||||
const VIEW_PAYOUTS = 1 << 9;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags_serde_impl!(ProjectPermissions, u64);
|
||||
|
||||
impl Default for ProjectPermissions {
|
||||
fn default() -> ProjectPermissions {
|
||||
ProjectPermissions::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectPermissions {
|
||||
pub fn get_permissions_by_role(
|
||||
role: &crate::models::users::Role,
|
||||
project_team_member: &Option<crate::database::models::TeamMember>, // team member of the user in the project
|
||||
organization_team_member: &Option<crate::database::models::TeamMember>, // team member of the user in the organization
|
||||
) -> Option<Self> {
|
||||
if role.is_admin() {
|
||||
return Some(ProjectPermissions::all());
|
||||
}
|
||||
|
||||
if let Some(member) = project_team_member {
|
||||
if member.accepted {
|
||||
return Some(member.permissions);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(member) = organization_team_member {
|
||||
if member.accepted {
|
||||
return Some(member.permissions);
|
||||
}
|
||||
}
|
||||
|
||||
if role.is_mod() {
|
||||
Some(
|
||||
ProjectPermissions::EDIT_DETAILS
|
||||
| ProjectPermissions::EDIT_BODY
|
||||
| ProjectPermissions::UPLOAD_VERSION,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct OrganizationPermissions: u64 {
|
||||
const EDIT_DETAILS = 1 << 0;
|
||||
const MANAGE_INVITES = 1 << 1;
|
||||
const REMOVE_MEMBER = 1 << 2;
|
||||
const EDIT_MEMBER = 1 << 3;
|
||||
const ADD_PROJECT = 1 << 4;
|
||||
const REMOVE_PROJECT = 1 << 5;
|
||||
const DELETE_ORGANIZATION = 1 << 6;
|
||||
const EDIT_MEMBER_DEFAULT_PERMISSIONS = 1 << 7; // Separate from EDIT_MEMBER
|
||||
const NONE = 0b0;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags_serde_impl!(OrganizationPermissions, u64);
|
||||
|
||||
impl Default for OrganizationPermissions {
|
||||
fn default() -> OrganizationPermissions {
|
||||
OrganizationPermissions::NONE
|
||||
}
|
||||
}
|
||||
|
||||
impl OrganizationPermissions {
|
||||
pub fn get_permissions_by_role(
|
||||
role: &crate::models::users::Role,
|
||||
team_member: &Option<crate::database::models::TeamMember>,
|
||||
) -> Option<Self> {
|
||||
if role.is_admin() {
|
||||
return Some(OrganizationPermissions::all());
|
||||
}
|
||||
|
||||
if let Some(member) = team_member {
|
||||
if member.accepted {
|
||||
return member.organization_permissions;
|
||||
}
|
||||
}
|
||||
if role.is_mod() {
|
||||
return Some(
|
||||
OrganizationPermissions::EDIT_DETAILS | OrganizationPermissions::ADD_PROJECT,
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A member of a team
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct TeamMember {
|
||||
/// The ID of the team this team member is a member of
|
||||
pub team_id: TeamId,
|
||||
/// The user associated with the member
|
||||
pub user: User,
|
||||
/// The role of the user in the team
|
||||
pub role: String,
|
||||
/// Is the user the owner of the team?
|
||||
pub is_owner: bool,
|
||||
/// A bitset containing the user's permissions in this team.
|
||||
/// In an organization-controlled project, these are the unique overriding permissions for the user's role for any project in the organization, if they exist.
|
||||
/// In an organization, these are the default project permissions for any project in the organization.
|
||||
/// Not optional- only None if they are being hidden from the user.
|
||||
pub permissions: Option<ProjectPermissions>,
|
||||
|
||||
/// A bitset containing the user's permissions in this organization.
|
||||
/// In a project team, this is None.
|
||||
pub organization_permissions: Option<OrganizationPermissions>,
|
||||
|
||||
/// Whether the user has joined the team or is just invited to it
|
||||
pub accepted: bool,
|
||||
|
||||
#[serde(with = "rust_decimal::serde::float_option")]
|
||||
/// Payouts split. This is a weighted average. For example. if a team has two members with this
|
||||
/// value set to 25.0 for both members, they split revenue 50/50
|
||||
pub payouts_split: Option<Decimal>,
|
||||
/// Ordering of the member in the list
|
||||
pub ordering: i64,
|
||||
}
|
||||
|
||||
impl TeamMember {
|
||||
pub fn from(
|
||||
data: crate::database::models::team_item::TeamMember,
|
||||
user: crate::database::models::User,
|
||||
override_permissions: bool,
|
||||
) -> Self {
|
||||
let user: User = user.into();
|
||||
Self::from_model(data, user, override_permissions)
|
||||
}
|
||||
|
||||
// Use the User model directly instead of the database model,
|
||||
// if already available.
|
||||
// (Avoids a db query in some cases)
|
||||
pub fn from_model(
|
||||
data: crate::database::models::team_item::TeamMember,
|
||||
user: crate::models::users::User,
|
||||
override_permissions: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
team_id: data.team_id.into(),
|
||||
user,
|
||||
role: data.role,
|
||||
is_owner: data.is_owner,
|
||||
permissions: if override_permissions {
|
||||
None
|
||||
} else {
|
||||
Some(data.permissions)
|
||||
},
|
||||
organization_permissions: if override_permissions {
|
||||
None
|
||||
} else {
|
||||
data.organization_permissions
|
||||
},
|
||||
accepted: data.accepted,
|
||||
payouts_split: if override_permissions {
|
||||
None
|
||||
} else {
|
||||
Some(data.payouts_split)
|
||||
},
|
||||
ordering: data.ordering,
|
||||
}
|
||||
}
|
||||
}
|
||||
137
apps/labrinth/src/models/v3/threads.rs
Normal file
137
apps/labrinth/src/models/v3/threads.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use super::ids::{Base62Id, ImageId};
|
||||
use crate::models::ids::{ProjectId, ReportId};
|
||||
use crate::models::projects::ProjectStatus;
|
||||
use crate::models::users::{User, UserId};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct ThreadId(pub u64);
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct ThreadMessageId(pub u64);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Thread {
|
||||
pub id: ThreadId,
|
||||
#[serde(rename = "type")]
|
||||
pub type_: ThreadType,
|
||||
pub project_id: Option<ProjectId>,
|
||||
pub report_id: Option<ReportId>,
|
||||
pub messages: Vec<ThreadMessage>,
|
||||
pub members: Vec<User>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ThreadMessage {
|
||||
pub id: ThreadMessageId,
|
||||
pub author_id: Option<UserId>,
|
||||
pub body: MessageBody,
|
||||
pub created: DateTime<Utc>,
|
||||
pub hide_identity: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum MessageBody {
|
||||
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 ThreadType {
|
||||
Report,
|
||||
Project,
|
||||
DirectMessage,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ThreadType {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadType {
|
||||
// These are constant, so this can remove unneccessary allocations (`to_string`)
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ThreadType::Report => "report",
|
||||
ThreadType::Project => "project",
|
||||
ThreadType::DirectMessage => "direct_message",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> ThreadType {
|
||||
match string {
|
||||
"report" => ThreadType::Report,
|
||||
"project" => ThreadType::Project,
|
||||
"direct_message" => ThreadType::DirectMessage,
|
||||
_ => ThreadType::DirectMessage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
pub fn from(data: crate::database::models::Thread, users: Vec<User>, user: &User) -> Self {
|
||||
let thread_type = data.type_;
|
||||
|
||||
Thread {
|
||||
id: data.id.into(),
|
||||
type_: thread_type,
|
||||
project_id: data.project_id.map(|x| x.into()),
|
||||
report_id: data.report_id.map(|x| x.into()),
|
||||
messages: data
|
||||
.messages
|
||||
.into_iter()
|
||||
.filter(|x| {
|
||||
if let MessageBody::Text { private, .. } = x.body {
|
||||
!private || user.role.is_mod()
|
||||
} else if let MessageBody::Deleted { private, .. } = x.body {
|
||||
!private || user.role.is_mod()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.map(|x| ThreadMessage::from(x, user))
|
||||
.collect(),
|
||||
members: users,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadMessage {
|
||||
pub fn from(data: crate::database::models::ThreadMessage, user: &User) -> Self {
|
||||
Self {
|
||||
id: data.id.into(),
|
||||
author_id: if data.hide_identity && !user.role.is_mod() {
|
||||
None
|
||||
} else {
|
||||
data.author_id.map(|x| x.into())
|
||||
},
|
||||
body: data.body,
|
||||
created: data.created,
|
||||
hide_identity: data.hide_identity,
|
||||
}
|
||||
}
|
||||
}
|
||||
187
apps/labrinth/src/models/v3/users.rs
Normal file
187
apps/labrinth/src/models/v3/users.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use super::ids::Base62Id;
|
||||
use crate::{auth::AuthProvider, bitflags_serde_impl};
|
||||
use chrono::{DateTime, Utc};
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct UserId(pub u64);
|
||||
|
||||
pub const DELETED_USER: UserId = UserId(127155982985829);
|
||||
|
||||
bitflags::bitflags! {
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Badges: u64 {
|
||||
const MIDAS = 1 << 0;
|
||||
const EARLY_MODPACK_ADOPTER = 1 << 1;
|
||||
const EARLY_RESPACK_ADOPTER = 1 << 2;
|
||||
const EARLY_PLUGIN_ADOPTER = 1 << 3;
|
||||
const ALPHA_TESTER = 1 << 4;
|
||||
const CONTRIBUTOR = 1 << 5;
|
||||
const TRANSLATOR = 1 << 6;
|
||||
|
||||
const ALL = 0b1111111;
|
||||
const NONE = 0b0;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags_serde_impl!(Badges, u64);
|
||||
|
||||
impl Default for Badges {
|
||||
fn default() -> Badges {
|
||||
Badges::NONE
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct User {
|
||||
pub id: UserId,
|
||||
pub username: 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 stripe_customer_id: Option<String>,
|
||||
|
||||
// DEPRECATED. Always returns None
|
||||
pub github_id: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct UserPayoutData {
|
||||
pub paypal_address: Option<String>,
|
||||
pub paypal_country: Option<String>,
|
||||
pub venmo_handle: Option<String>,
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub balance: Decimal,
|
||||
}
|
||||
|
||||
use crate::database::models::user_item::User as DBUser;
|
||||
impl From<DBUser> for User {
|
||||
fn from(data: DBUser) -> Self {
|
||||
Self {
|
||||
id: data.id.into(),
|
||||
username: data.username,
|
||||
email: None,
|
||||
email_verified: None,
|
||||
avatar_url: data.avatar_url,
|
||||
bio: data.bio,
|
||||
created: data.created,
|
||||
role: Role::from_string(&data.role),
|
||||
badges: data.badges,
|
||||
payout_data: None,
|
||||
auth_providers: None,
|
||||
has_password: None,
|
||||
has_totp: None,
|
||||
github_id: None,
|
||||
stripe_customer_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn from_full(db_user: DBUser) -> Self {
|
||||
let mut auth_providers = Vec::new();
|
||||
|
||||
if db_user.github_id.is_some() {
|
||||
auth_providers.push(AuthProvider::GitHub)
|
||||
}
|
||||
if db_user.gitlab_id.is_some() {
|
||||
auth_providers.push(AuthProvider::GitLab)
|
||||
}
|
||||
if db_user.discord_id.is_some() {
|
||||
auth_providers.push(AuthProvider::Discord)
|
||||
}
|
||||
if db_user.google_id.is_some() {
|
||||
auth_providers.push(AuthProvider::Google)
|
||||
}
|
||||
if db_user.microsoft_id.is_some() {
|
||||
auth_providers.push(AuthProvider::Microsoft)
|
||||
}
|
||||
if db_user.steam_id.is_some() {
|
||||
auth_providers.push(AuthProvider::Steam)
|
||||
}
|
||||
if db_user.paypal_id.is_some() {
|
||||
auth_providers.push(AuthProvider::PayPal)
|
||||
}
|
||||
|
||||
Self {
|
||||
id: UserId::from(db_user.id),
|
||||
username: db_user.username,
|
||||
email: db_user.email,
|
||||
email_verified: Some(db_user.email_verified),
|
||||
avatar_url: db_user.avatar_url,
|
||||
bio: db_user.bio,
|
||||
created: db_user.created,
|
||||
role: Role::from_string(&db_user.role),
|
||||
badges: db_user.badges,
|
||||
auth_providers: Some(auth_providers),
|
||||
has_password: Some(db_user.password.is_some()),
|
||||
has_totp: Some(db_user.totp_secret.is_some()),
|
||||
github_id: None,
|
||||
payout_data: Some(UserPayoutData {
|
||||
paypal_address: db_user.paypal_email,
|
||||
paypal_country: db_user.paypal_country,
|
||||
venmo_handle: db_user.venmo_handle,
|
||||
balance: Decimal::ZERO,
|
||||
}),
|
||||
stripe_customer_id: db_user.stripe_customer_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Role {
|
||||
Developer,
|
||||
Moderator,
|
||||
Admin,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Role {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn from_string(string: &str) -> Role {
|
||||
match string {
|
||||
"admin" => Role::Admin,
|
||||
"moderator" => Role::Moderator,
|
||||
_ => Role::Developer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Role::Developer => "developer",
|
||||
Role::Moderator => "moderator",
|
||||
Role::Admin => "admin",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_mod(&self) -> bool {
|
||||
match self {
|
||||
Role::Developer => false,
|
||||
Role::Moderator | Role::Admin => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_admin(&self) -> bool {
|
||||
match self {
|
||||
Role::Developer | Role::Moderator => false,
|
||||
Role::Admin => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user