You've already forked AstralRinth
forked from didirus/AstralRinth
Search test + v3 (#731)
* search patch for accurate loader/gv filtering * backup * basic search test * finished test * incomplete commit; backing up * Working multipat reroute backup * working rough draft v3 * most tests passing * works * search v2 conversion * added some tags.rs v2 conversions * Worked through warnings, unwraps, prints * refactors * new search test * version files changes fixes * redesign to revs * removed old caches * removed games * fmt clippy * merge conflicts * fmt, prepare * moved v2 routes over to v3 * fixes; tests passing * project type changes * moved files over * fmt, clippy, prepare, etc * loaders to loader_fields, added tests * fmt, clippy, prepare * fixed sorting bug * reversed back- wrong order for consistency * fmt; clippy; prepare --------- Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
111
src/models/v3/analytics.rs
Normal file
111
src/models/v3/analytics.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use clickhouse::Row;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::net::Ipv6Addr;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Row, Serialize, Deserialize, Clone)]
|
||||
pub struct Download {
|
||||
#[serde(with = "uuid::serde::compact")]
|
||||
pub id: Uuid,
|
||||
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)>,
|
||||
}
|
||||
|
||||
impl PartialEq<Self> for Download {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Download {}
|
||||
|
||||
impl Hash for Download {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Row, Serialize, Deserialize, Clone)]
|
||||
pub struct PageView {
|
||||
#[serde(with = "uuid::serde::compact")]
|
||||
pub id: Uuid,
|
||||
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,
|
||||
|
||||
// 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)>,
|
||||
}
|
||||
|
||||
impl PartialEq<Self> for PageView {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PageView {}
|
||||
|
||||
impl Hash for PageView {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Row, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Playtime {
|
||||
#[serde(with = "uuid::serde::compact")]
|
||||
pub id: Uuid,
|
||||
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,
|
||||
}
|
||||
|
||||
impl PartialEq<Self> for Playtime {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Playtime {}
|
||||
|
||||
impl Hash for Playtime {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
132
src/models/v3/collections.rs
Normal file
132
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 title: String,
|
||||
/// A short description of the collection.
|
||||
pub description: 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,
|
||||
title: c.title,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/models/v3/error.rs
Normal file
8
src/models/v3/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: &'a str,
|
||||
}
|
||||
212
src/models/v3/ids.rs
Normal file
212
src/models/v3/ids.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use thiserror::Error;
|
||||
|
||||
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::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;
|
||||
|
||||
/// 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 {
|
||||
use rand::Rng;
|
||||
assert!(n > 0 && n <= 11);
|
||||
// gen_range is [low, high): max value is `MULTIPLES[n] - 1`,
|
||||
// which is n characters long when encoded
|
||||
rng.gen_range(MULTIPLES[n - 1]..MULTIPLES[n])
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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
src/models/v3/images.rs
Normal file
124
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/models/v3/mod.rs
Normal file
16
src/models/v3/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
pub mod analytics;
|
||||
pub mod collections;
|
||||
pub mod error;
|
||||
pub mod ids;
|
||||
pub mod images;
|
||||
pub mod notifications;
|
||||
pub mod oauth_clients;
|
||||
pub mod organizations;
|
||||
pub mod pack;
|
||||
pub mod pats;
|
||||
pub mod projects;
|
||||
pub mod reports;
|
||||
pub mod sessions;
|
||||
pub mod teams;
|
||||
pub mod threads;
|
||||
pub mod users;
|
||||
231
src/models/v3/notifications.rs
Normal file
231
src/models/v3/notifications.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
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,
|
||||
|
||||
// DEPRECATED: use body field instead
|
||||
#[serde(rename = "type")]
|
||||
pub type_: Option<String>,
|
||||
pub title: 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>,
|
||||
title: String,
|
||||
text: String,
|
||||
link: String,
|
||||
actions: Vec<NotificationAction>,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<DBNotification> for Notification {
|
||||
fn from(notif: DBNotification) -> Self {
|
||||
let (type_, title, text, link, actions) = {
|
||||
match ¬if.body {
|
||||
NotificationBody::ProjectUpdate {
|
||||
project_id,
|
||||
version_id,
|
||||
} => (
|
||||
Some("project_update".to_string()),
|
||||
"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,
|
||||
..
|
||||
} => (
|
||||
Some("team_invite".to_string()),
|
||||
"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 {
|
||||
title: "Accept".to_string(),
|
||||
action_route: ("POST".to_string(), format!("team/{team_id}/join")),
|
||||
},
|
||||
NotificationAction {
|
||||
title: "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,
|
||||
..
|
||||
} => (
|
||||
Some("organization_invite".to_string()),
|
||||
"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 {
|
||||
title: "Accept".to_string(),
|
||||
action_route: ("POST".to_string(), format!("team/{team_id}/join")),
|
||||
},
|
||||
NotificationAction {
|
||||
title: "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,
|
||||
} => (
|
||||
Some("status_change".to_string()),
|
||||
"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,
|
||||
..
|
||||
} => (
|
||||
Some("moderator_message".to_string()),
|
||||
"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 {
|
||||
notification_type,
|
||||
title,
|
||||
text,
|
||||
link,
|
||||
actions,
|
||||
} => (
|
||||
notification_type.clone(),
|
||||
title.clone(),
|
||||
text.clone(),
|
||||
link.clone(),
|
||||
actions.clone().into_iter().map(Into::into).collect(),
|
||||
),
|
||||
NotificationBody::Unknown => (
|
||||
None,
|
||||
"".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,
|
||||
|
||||
// DEPRECATED
|
||||
type_,
|
||||
title,
|
||||
text,
|
||||
link,
|
||||
actions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct NotificationAction {
|
||||
pub title: 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 {
|
||||
title: act.title,
|
||||
action_route: (act.action_route_method, act.action_route),
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/models/v3/oauth_clients.rs
Normal file
115
src/models/v3/oauth_clients.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
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,
|
||||
}
|
||||
|
||||
#[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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/models/v3/organizations.rs
Normal file
49
src/models/v3/organizations.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use super::{
|
||||
ids::{Base62Id, TeamId},
|
||||
teams::TeamMember,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The ID of a team
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[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 title (and slug) of the organization
|
||||
pub title: 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(),
|
||||
title: data.title,
|
||||
team_id: data.team_id.into(),
|
||||
description: data.description,
|
||||
members: team_members,
|
||||
icon_url: data.icon_url,
|
||||
color: data.color,
|
||||
}
|
||||
}
|
||||
}
|
||||
109
src/models/v3/pack.rs
Normal file
109
src/models/v3/pack.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use crate::{models::projects::SideType, util::env::parse_strings_from_var};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate, Eq, PartialEq)]
|
||||
#[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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PackFile {
|
||||
pub path: String,
|
||||
pub hashes: std::collections::HashMap<PackFileHash, String>,
|
||||
pub env: Option<std::collections::HashMap<EnvType, SideType>>,
|
||||
#[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)]
|
||||
#[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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EnvType {
|
||||
Client,
|
||||
Server,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq)]
|
||||
#[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
src/models/v3/pats.rs
Normal file
241
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)]
|
||||
#[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()
|
||||
);
|
||||
}
|
||||
}
|
||||
807
src/models/v3/projects.rs
Normal file
807
src/models/v3/projects.rs
Normal file
@@ -0,0 +1,807 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::ids::{Base62Id, OrganizationId};
|
||||
use super::teams::TeamId;
|
||||
use super::users::UserId;
|
||||
use crate::database::models::project_item::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)]
|
||||
#[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: TeamId,
|
||||
/// The optional organization of people that have ownership of this project.
|
||||
pub organization: Option<OrganizationId>,
|
||||
/// The title or name of the project.
|
||||
pub title: String,
|
||||
/// A short description of the project.
|
||||
pub description: String,
|
||||
/// A long form description of the project.
|
||||
pub body: String,
|
||||
/// The link to the long description of the project. Deprecated, always None
|
||||
pub body_url: Option<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>,
|
||||
/// An optional link to where to submit bugs or issues with the project.
|
||||
pub issues_url: Option<String>,
|
||||
/// An optional link to the source code for the project.
|
||||
pub source_url: Option<String>,
|
||||
/// An optional link to the project's wiki page or other relevant information.
|
||||
pub wiki_url: Option<String>,
|
||||
/// An optional link to the project's discord
|
||||
pub discord_url: Option<String>,
|
||||
/// An optional list of all donation links the project has
|
||||
pub donation_urls: Option<Vec<DonationLink>>,
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl From<QueryProject> for Project {
|
||||
fn from(data: QueryProject) -> Self {
|
||||
let m = data.inner;
|
||||
Self {
|
||||
id: m.id.into(),
|
||||
slug: m.slug,
|
||||
project_types: data.project_types,
|
||||
games: data.games,
|
||||
team: m.team_id.into(),
|
||||
organization: m.organization_id.map(|i| i.into()),
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
body: m.body,
|
||||
body_url: None,
|
||||
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,
|
||||
issues_url: m.issues_url,
|
||||
source_url: m.source_url,
|
||||
wiki_url: m.wiki_url,
|
||||
discord_url: m.discord_url,
|
||||
donation_urls: Some(
|
||||
data.donation_urls
|
||||
.into_iter()
|
||||
.map(|d| DonationLink {
|
||||
id: d.platform_short,
|
||||
platform: d.platform_name,
|
||||
url: d.url,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
gallery: data
|
||||
.gallery_items
|
||||
.into_iter()
|
||||
.map(|x| GalleryItem {
|
||||
url: x.image_url,
|
||||
featured: x.featured,
|
||||
title: x.title,
|
||||
description: x.description,
|
||||
created: x.created,
|
||||
ordering: x.ordering,
|
||||
})
|
||||
.collect(),
|
||||
color: m.color,
|
||||
thread_id: data.thread_id.into(),
|
||||
monetization_status: m.monetization_status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct GalleryItem {
|
||||
pub url: String,
|
||||
pub featured: bool,
|
||||
pub title: 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>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SideType {
|
||||
Required,
|
||||
Optional,
|
||||
Unsupported,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SideType {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl SideType {
|
||||
// These are constant, so this can remove unneccessary allocations (`to_string`)
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
SideType::Required => "required",
|
||||
SideType::Optional => "optional",
|
||||
SideType::Unsupported => "unsupported",
|
||||
SideType::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> SideType {
|
||||
match string {
|
||||
"required" => SideType::Required,
|
||||
"optional" => SideType::Optional,
|
||||
"unsupported" => SideType::Unsupported,
|
||||
_ => SideType::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 DonationLink {
|
||||
pub id: String,
|
||||
pub platform: String,
|
||||
#[validate(
|
||||
custom(function = "crate::util::validate::validate_url"),
|
||||
length(max = 2048)
|
||||
)]
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
/// A link to the changelog for this version of the project. Deprecated, always None
|
||||
pub changelog_url: Option<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,
|
||||
changelog_url: None,
|
||||
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)]
|
||||
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)]
|
||||
#[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
src/models/v3/reports.rs
Normal file
73
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
src/models/v3/sessions.rs
Normal file
60
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)]
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
200
src/models/v3/teams.rs
Normal file
200
src/models/v3/teams.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
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 OWNER_ROLE: &str = "Owner";
|
||||
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::UPLOAD_VERSION | ProjectPermissions::DELETE_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
/// 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,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
132
src/models/v3/threads.rs
Normal file
132
src/models/v3/threads.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
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>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
true
|
||||
}
|
||||
})
|
||||
.map(|x| ThreadMessage {
|
||||
id: x.id.into(),
|
||||
author_id: if users
|
||||
.iter()
|
||||
.find(|y| x.author_id == Some(y.id.into()))
|
||||
.map(|x| x.role.is_mod() && !user.role.is_mod())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
x.author_id.map(|x| x.into())
|
||||
},
|
||||
body: x.body,
|
||||
created: x.created,
|
||||
})
|
||||
.collect(),
|
||||
members: users
|
||||
.into_iter()
|
||||
.filter(|x| !x.role.is_mod() || user.role.is_mod())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
222
src/models/v3/users.rs
Normal file
222
src/models/v3/users.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use super::ids::Base62Id;
|
||||
use crate::auth::flows::AuthProvider;
|
||||
use crate::bitflags_serde_impl;
|
||||
use chrono::{DateTime, Utc};
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
#[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 {
|
||||
// 1 << 0 unused - ignore + replace with something later
|
||||
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 name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub created: DateTime<Utc>,
|
||||
pub role: Role,
|
||||
pub badges: Badges,
|
||||
|
||||
pub auth_providers: Option<Vec<AuthProvider>>,
|
||||
pub email: Option<String>,
|
||||
pub email_verified: Option<bool>,
|
||||
pub has_password: Option<bool>,
|
||||
pub has_totp: Option<bool>,
|
||||
pub payout_data: Option<UserPayoutData>,
|
||||
|
||||
// DEPRECATED. Always returns None
|
||||
pub github_id: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct UserPayoutData {
|
||||
pub balance: Decimal,
|
||||
pub trolley_id: Option<String>,
|
||||
pub trolley_status: Option<RecipientStatus>,
|
||||
}
|
||||
|
||||
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,
|
||||
name: data.name,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RecipientStatus {
|
||||
Active,
|
||||
Incomplete,
|
||||
Disabled,
|
||||
Archived,
|
||||
Suspended,
|
||||
Blocked,
|
||||
}
|
||||
|
||||
impl RecipientStatus {
|
||||
pub fn from_string(string: &str) -> RecipientStatus {
|
||||
match string {
|
||||
"active" => RecipientStatus::Active,
|
||||
"incomplete" => RecipientStatus::Incomplete,
|
||||
"disabled" => RecipientStatus::Disabled,
|
||||
"archived" => RecipientStatus::Archived,
|
||||
"suspended" => RecipientStatus::Suspended,
|
||||
"blocked" => RecipientStatus::Blocked,
|
||||
_ => RecipientStatus::Disabled,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
RecipientStatus::Active => "active",
|
||||
RecipientStatus::Incomplete => "incomplete",
|
||||
RecipientStatus::Disabled => "disabled",
|
||||
RecipientStatus::Archived => "archived",
|
||||
RecipientStatus::Suspended => "suspended",
|
||||
RecipientStatus::Blocked => "blocked",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Payout {
|
||||
pub created: DateTime<Utc>,
|
||||
pub amount: Decimal,
|
||||
pub status: PayoutStatus,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PayoutStatus {
|
||||
Pending,
|
||||
Failed,
|
||||
Processed,
|
||||
Returned,
|
||||
Processing,
|
||||
}
|
||||
|
||||
impl PayoutStatus {
|
||||
pub fn from_string(string: &str) -> PayoutStatus {
|
||||
match string {
|
||||
"pending" => PayoutStatus::Pending,
|
||||
"failed" => PayoutStatus::Failed,
|
||||
"processed" => PayoutStatus::Processed,
|
||||
"returned" => PayoutStatus::Returned,
|
||||
"processing" => PayoutStatus::Processing,
|
||||
_ => PayoutStatus::Processing,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
PayoutStatus::Pending => "pending",
|
||||
PayoutStatus::Failed => "failed",
|
||||
PayoutStatus::Processed => "processed",
|
||||
PayoutStatus::Returned => "returned",
|
||||
PayoutStatus::Processing => "processing",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_failed(&self) -> bool {
|
||||
match self {
|
||||
PayoutStatus::Pending => false,
|
||||
PayoutStatus::Failed => true,
|
||||
PayoutStatus::Processed => false,
|
||||
PayoutStatus::Returned => true,
|
||||
PayoutStatus::Processing => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user