You've already forked AstralRinth
Add utoipa Swagger UI support (#4602)
* Add utoipa Swagger UI support * remove unused code * remove unused code * consistency with trailing slash
This commit is contained in:
112
Cargo.lock
generated
112
Cargo.lock
generated
@@ -478,6 +478,7 @@ dependencies = [
|
||||
"serde_cbor",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"utoipa",
|
||||
"uuid 1.18.1",
|
||||
]
|
||||
|
||||
@@ -4678,6 +4679,9 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"utoipa",
|
||||
"utoipa-actix-web",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid 1.18.1",
|
||||
"validator",
|
||||
"webp",
|
||||
@@ -7335,6 +7339,40 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.106",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.21.3"
|
||||
@@ -10309,6 +10347,66 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "utoipa"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
|
||||
dependencies = [
|
||||
"indexmap 2.11.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa-gen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-actix-web"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7eda9c23c05af0fb812f6a177514047331dac4851a2c8e9c4b895d6d826967f"
|
||||
dependencies = [
|
||||
"actix-service",
|
||||
"actix-web",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-gen"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-swagger-ui"
|
||||
version = "9.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"base64 0.22.1",
|
||||
"mime_guess",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui-vendored",
|
||||
"zip 3.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-swagger-ui-vendored"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "0.8.2"
|
||||
@@ -11687,6 +11785,20 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap 2.11.4",
|
||||
"memchr",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
|
||||
@@ -192,6 +192,9 @@ tracing-subscriber = "0.3.20"
|
||||
typed-path = "0.12.0"
|
||||
url = "2.5.7"
|
||||
urlencoding = "2.1.3"
|
||||
utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "decimal"] }
|
||||
utoipa-actix-web = { version = "0.1.2" }
|
||||
utoipa-swagger-ui = { version = "9.0.2", features = ["actix-web", "vendored"] }
|
||||
uuid = "1.18.1"
|
||||
validator = "0.20.0"
|
||||
webp = { version = "0.3.1", default-features = false }
|
||||
|
||||
@@ -120,6 +120,9 @@ tracing-ecs = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
url = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
utoipa = { workspace = true }
|
||||
utoipa-actix-web = { workspace = true }
|
||||
utoipa-swagger-ui = { workspace = true }
|
||||
uuid = { workspace = true, features = ["fast-rng", "serde", "v4"] }
|
||||
validator = { workspace = true, features = ["derive"] }
|
||||
webp = { workspace = true }
|
||||
|
||||
@@ -345,6 +345,13 @@ pub fn app_config(
|
||||
.default_service(web::get().wrap(default_cors()).to(routes::not_found));
|
||||
}
|
||||
|
||||
pub fn utoipa_app_config(
|
||||
cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
|
||||
_labrinth_config: LabrinthConfig,
|
||||
) {
|
||||
cfg.configure(routes::v3::utoipa_config);
|
||||
}
|
||||
|
||||
// This is so that env vars not used immediately don't panic at runtime
|
||||
pub fn check_env_vars() -> bool {
|
||||
let mut failed = false;
|
||||
|
||||
@@ -14,6 +14,7 @@ use labrinth::util::anrok;
|
||||
use labrinth::util::env::parse_var;
|
||||
use labrinth::util::gotenberg::GotenbergClient;
|
||||
use labrinth::util::ratelimit::rate_limit_middleware;
|
||||
use labrinth::utoipa_app_config;
|
||||
use labrinth::{check_env_vars, clickhouse, database, file_hosting};
|
||||
use std::ffi::CStr;
|
||||
use std::str::FromStr;
|
||||
@@ -25,6 +26,9 @@ use tracing_ecs::ECSLayerBuilder;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_actix_web::AppExt;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[global_allocator]
|
||||
@@ -293,6 +297,12 @@ async fn main() -> std::io::Result<()> {
|
||||
.wrap(from_fn(rate_limit_middleware))
|
||||
.wrap(actix_web::middleware::Compress::default())
|
||||
.wrap(sentry_actix::Sentry::new())
|
||||
.into_utoipa_app()
|
||||
.configure(|cfg| utoipa_app_config(cfg, labrinth_config.clone()))
|
||||
.openapi_service(|api| SwaggerUi::new("/docs/swagger-ui/{_:.*}")
|
||||
.config(utoipa_swagger_ui::Config::default().try_it_out_enabled(true))
|
||||
.url("/docs/openapi.json", ApiDoc::openapi().merge_from(api)))
|
||||
.into_app()
|
||||
.configure(|cfg| app_config(cfg, labrinth_config.clone()))
|
||||
})
|
||||
.bind(dotenvy::var("BIND_ADDR").unwrap())?
|
||||
@@ -300,6 +310,10 @@ async fn main() -> std::io::Result<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(utoipa::OpenApi)]
|
||||
#[openapi(info(title = "Labrinth"))]
|
||||
struct ApiDoc;
|
||||
|
||||
fn log_error(err: &actix_web::Error) {
|
||||
if err.as_response_error().status_code().is_client_error() {
|
||||
tracing::debug!(
|
||||
|
||||
@@ -77,12 +77,8 @@ pub fn root_config(cfg: &mut web::ServiceConfig) {
|
||||
}.boxed_local()
|
||||
})
|
||||
);
|
||||
cfg.service(
|
||||
web::scope("")
|
||||
.wrap(default_cors())
|
||||
.service(index::index_get)
|
||||
.service(Files::new("/", "assets/")),
|
||||
);
|
||||
cfg.service(index::index_get);
|
||||
cfg.service(Files::new("/", "assets/"));
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
//! requests, you have to zip together M arrays of N elements
|
||||
//! - this makes it inconvenient to have separate endpoints
|
||||
|
||||
mod old;
|
||||
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
use actix_web::{HttpRequest, web};
|
||||
use actix_web::{HttpRequest, post, web};
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use futures::StreamExt;
|
||||
use rust_decimal::Decimal;
|
||||
@@ -32,10 +34,9 @@ use crate::{
|
||||
routes::ApiError,
|
||||
};
|
||||
|
||||
// TODO: this service `analytics` is shadowed by `analytics_get_old`'s
|
||||
// see the TODO in `analytics_get_old.rs`
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::scope("analytics").route("", web::post().to(get)));
|
||||
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
|
||||
cfg.service(fetch_analytics);
|
||||
cfg.configure(old::config);
|
||||
}
|
||||
|
||||
// request
|
||||
@@ -43,7 +44,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
/// Requests analytics data, aggregating over all possible analytics sources
|
||||
/// like projects and affiliate codes, returning the data in a list of time
|
||||
/// slices.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct GetRequest {
|
||||
/// What time range to return statistics for.
|
||||
pub time_range: TimeRange,
|
||||
@@ -52,7 +53,7 @@ pub struct GetRequest {
|
||||
}
|
||||
|
||||
/// Time range for fetching analytics.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct TimeRange {
|
||||
/// When to start including data.
|
||||
pub start: DateTime<Utc>,
|
||||
@@ -68,20 +69,22 @@ pub struct TimeRange {
|
||||
|
||||
/// Determines how many time slices between the start and end will be
|
||||
/// included, and how fine-grained those time slices will be.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TimeRangeResolution {
|
||||
/// Use a set number of time slices, with the resolution being determined
|
||||
/// automatically.
|
||||
#[schema(value_type = u64)]
|
||||
Slices(NonZeroU64),
|
||||
/// Each time slice will be a set number of minutes long, and the number of
|
||||
/// slices is determined automatically.
|
||||
#[schema(value_type = u64)]
|
||||
Minutes(NonZeroU64),
|
||||
}
|
||||
|
||||
/// What metrics the caller would like to receive from this analytics get
|
||||
/// request.
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ReturnMetrics {
|
||||
/// How many times a project page has been viewed.
|
||||
pub project_views: Option<Metrics<ProjectViewsField>>,
|
||||
@@ -90,11 +93,15 @@ pub struct ReturnMetrics {
|
||||
/// How long users have been playing a project.
|
||||
pub project_playtime: Option<Metrics<ProjectPlaytimeField>>,
|
||||
/// How much payout revenue a project has generated.
|
||||
pub project_revenue: Option<Metrics<()>>,
|
||||
pub project_revenue: Option<Metrics<Unit>>,
|
||||
}
|
||||
|
||||
/// Replacement for `()` because of a `utoipa` limitation.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct Unit {}
|
||||
|
||||
/// See [`ReturnMetrics`].
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct Metrics<F> {
|
||||
/// When collecting metrics, what fields do we want to group the results by?
|
||||
///
|
||||
@@ -114,7 +121,9 @@ pub struct Metrics<F> {
|
||||
}
|
||||
|
||||
/// Fields for [`ReturnMetrics::project_views`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProjectViewsField {
|
||||
/// Project ID.
|
||||
@@ -132,7 +141,9 @@ pub enum ProjectViewsField {
|
||||
}
|
||||
|
||||
/// Fields for [`ReturnMetrics::project_downloads`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProjectDownloadsField {
|
||||
/// Project ID.
|
||||
@@ -150,7 +161,9 @@ pub enum ProjectDownloadsField {
|
||||
}
|
||||
|
||||
/// Fields for [`ReturnMetrics::project_playtime`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProjectPlaytimeField {
|
||||
/// Project ID.
|
||||
@@ -177,15 +190,15 @@ pub const MAX_TIME_SLICES: usize = 1024;
|
||||
/// This is a list of N [`TimeSlice`]s, where each slice represents an equal
|
||||
/// time interval of metrics collection. The number of slices is determined
|
||||
/// by [`GetRequest::time_range`].
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct GetResponse(pub Vec<TimeSlice>);
|
||||
#[derive(Debug, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct FetchResponse(pub Vec<TimeSlice>);
|
||||
|
||||
/// Single time interval of metrics collection.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct TimeSlice(pub Vec<AnalyticsData>);
|
||||
|
||||
/// Metrics collected in a single [`TimeSlice`].
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(untagged)] // the presence of `source_project`, `source_affiliate_code` determines the kind
|
||||
pub enum AnalyticsData {
|
||||
/// Project metrics.
|
||||
@@ -194,7 +207,7 @@ pub enum AnalyticsData {
|
||||
}
|
||||
|
||||
/// Project metrics.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ProjectAnalytics {
|
||||
/// What project these metrics are for.
|
||||
source_project: ProjectId,
|
||||
@@ -213,7 +226,7 @@ impl ProjectAnalytics {
|
||||
/// Project metrics of a specific kind.
|
||||
///
|
||||
/// If a field is not included in [`Metrics::bucket_by`], it will be [`None`].
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "snake_case", tag = "metric_kind")]
|
||||
pub enum ProjectMetrics {
|
||||
/// [`ReturnMetrics::project_views`].
|
||||
@@ -227,7 +240,7 @@ pub enum ProjectMetrics {
|
||||
}
|
||||
|
||||
/// [`ReturnMetrics::project_views`].
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ProjectViews {
|
||||
/// [`ProjectViewsField::Domain`].
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -246,7 +259,7 @@ pub struct ProjectViews {
|
||||
}
|
||||
|
||||
/// [`ReturnMetrics::project_downloads`].
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ProjectDownloads {
|
||||
/// [`ProjectDownloadsField::Domain`].
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -265,7 +278,7 @@ pub struct ProjectDownloads {
|
||||
}
|
||||
|
||||
/// [`ReturnMetrics::project_playtime`].
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ProjectPlaytime {
|
||||
/// [`ProjectPlaytimeField::VersionId`].
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -281,7 +294,7 @@ pub struct ProjectPlaytime {
|
||||
}
|
||||
|
||||
/// [`ReturnMetrics::project_revenue`].
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ProjectRevenue {
|
||||
/// Total revenue for this bucket.
|
||||
revenue: Decimal,
|
||||
@@ -414,14 +427,19 @@ mod query {
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
/// Fetches analytics data for the authorized user's projects.
|
||||
#[utoipa::path(
|
||||
responses((status = OK, body = inline(FetchResponse))),
|
||||
)]
|
||||
#[post("")]
|
||||
pub async fn fetch_analytics(
|
||||
http_req: HttpRequest,
|
||||
req: web::Json<GetRequest>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
clickhouse: web::Data<clickhouse::Client>,
|
||||
) -> Result<web::Json<GetResponse>, ApiError> {
|
||||
) -> Result<web::Json<FetchResponse>, ApiError> {
|
||||
let (scopes, user) = get_user_from_headers(
|
||||
&http_req,
|
||||
&**pool,
|
||||
@@ -655,7 +673,7 @@ pub async fn get(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(web::Json(GetResponse(time_slices)))
|
||||
Ok(web::Json(FetchResponse(time_slices)))
|
||||
}
|
||||
|
||||
fn none_if_empty(s: String) -> Option<String> {
|
||||
@@ -824,7 +842,7 @@ mod tests {
|
||||
let test_project_2 = ProjectId(456);
|
||||
let test_project_3 = ProjectId(789);
|
||||
|
||||
let src = GetResponse(vec![
|
||||
let src = FetchResponse(vec![
|
||||
TimeSlice(vec![
|
||||
AnalyticsData::Project(ProjectAnalytics {
|
||||
source_project: test_project_1,
|
||||
|
||||
@@ -7,13 +7,10 @@ use crate::models::teams::ProjectPermissions;
|
||||
use crate::{
|
||||
auth::get_user_from_headers,
|
||||
database::models::user_item,
|
||||
models::{
|
||||
ids::{ProjectId, VersionId},
|
||||
pats::Scopes,
|
||||
},
|
||||
models::{ids::ProjectId, pats::Scopes},
|
||||
queue::session::AuthQueue,
|
||||
};
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use actix_web::{HttpRequest, HttpResponse, get, web};
|
||||
use ariadne::ids::base62_impl::to_base62;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use eyre::eyre;
|
||||
@@ -24,28 +21,21 @@ use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("analytics")
|
||||
// TODO: since our service shadows analytics v2, we have to redirect here
|
||||
.route("", web::post().to(super::analytics_get::get))
|
||||
.route("playtime", web::get().to(playtimes_get))
|
||||
.route("views", web::get().to(views_get))
|
||||
.route("downloads", web::get().to(downloads_get))
|
||||
.route("revenue", web::get().to(revenue_get))
|
||||
.route(
|
||||
"countries/downloads",
|
||||
web::get().to(countries_downloads_get),
|
||||
)
|
||||
.route("countries/views", web::get().to(countries_views_get)),
|
||||
);
|
||||
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
|
||||
cfg.service(playtimes_get)
|
||||
.service(views_get)
|
||||
.service(downloads_get)
|
||||
.service(revenue_get)
|
||||
.service(countries_downloads_get)
|
||||
.service(countries_views_get);
|
||||
}
|
||||
|
||||
/// The json data to be passed to fetch analytic data
|
||||
/// The json data to be passed to fetch analytic data.
|
||||
///
|
||||
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
|
||||
/// start_date and end_date are optional, and default to two weeks ago, and the maximum date respectively.
|
||||
/// resolution_minutes is optional. This refers to the window by which we are looking (every day, every minute, etc) and defaults to 1440 (1 day)
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct GetData {
|
||||
// only one of project_ids or version_ids should be used
|
||||
// if neither are provided, all projects the user has access to will be used
|
||||
@@ -54,26 +44,12 @@ pub struct GetData {
|
||||
pub start_date: Option<DateTime<Utc>>, // defaults to 2 weeks ago
|
||||
pub end_date: Option<DateTime<Utc>>, // defaults to now
|
||||
|
||||
#[schema(value_type = Option<u32>, minimum = 1)]
|
||||
pub resolution_minutes: Option<NonZeroU32>, // defaults to 1 day. Ignored in routes that do not aggregate over a resolution (eg: /countries)
|
||||
}
|
||||
|
||||
/// Get playtime data for a set of projects or versions
|
||||
/// Data is returned as a hashmap of project/version ids to a hashmap of days to playtime data
|
||||
/// eg:
|
||||
/// {
|
||||
/// "4N1tEhnO": {
|
||||
/// "20230824": 23
|
||||
/// }
|
||||
///}
|
||||
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct FetchedPlaytime {
|
||||
pub time: u64,
|
||||
pub total_seconds: u64,
|
||||
pub loader_seconds: HashMap<String, u64>,
|
||||
pub game_version_seconds: HashMap<String, u64>,
|
||||
pub parent_seconds: HashMap<VersionId, u64>,
|
||||
}
|
||||
#[utoipa::path]
|
||||
#[get("/playtime")]
|
||||
pub async fn playtimes_get(
|
||||
req: HttpRequest,
|
||||
clickhouse: web::Data<clickhouse::Client>,
|
||||
@@ -134,7 +110,8 @@ pub async fn playtimes_get(
|
||||
Ok(HttpResponse::Ok().json(hm))
|
||||
}
|
||||
|
||||
/// Get view data for a set of projects or versions
|
||||
/// Get view data for a set of projects or versions.
|
||||
///
|
||||
/// Data is returned as a hashmap of project/version ids to a hashmap of days to views
|
||||
/// eg:
|
||||
/// {
|
||||
@@ -143,6 +120,8 @@ pub async fn playtimes_get(
|
||||
/// }
|
||||
///}
|
||||
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
|
||||
#[utoipa::path]
|
||||
#[get("/views")]
|
||||
pub async fn views_get(
|
||||
req: HttpRequest,
|
||||
clickhouse: web::Data<clickhouse::Client>,
|
||||
@@ -203,7 +182,8 @@ pub async fn views_get(
|
||||
Ok(HttpResponse::Ok().json(hm))
|
||||
}
|
||||
|
||||
/// Get download data for a set of projects or versions
|
||||
/// Get download data for a set of projects or versions.
|
||||
///
|
||||
/// Data is returned as a hashmap of project/version ids to a hashmap of days to downloads
|
||||
/// eg:
|
||||
/// {
|
||||
@@ -212,6 +192,8 @@ pub async fn views_get(
|
||||
/// }
|
||||
///}
|
||||
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
|
||||
#[utoipa::path]
|
||||
#[get("/downloads")]
|
||||
pub async fn downloads_get(
|
||||
req: HttpRequest,
|
||||
clickhouse: web::Data<clickhouse::Client>,
|
||||
@@ -273,7 +255,8 @@ pub async fn downloads_get(
|
||||
Ok(HttpResponse::Ok().json(hm))
|
||||
}
|
||||
|
||||
/// Get payout data for a set of projects
|
||||
/// Get payout data for a set of projects.
|
||||
///
|
||||
/// Data is returned as a hashmap of project ids to a hashmap of days to amount earned per day
|
||||
/// eg:
|
||||
/// {
|
||||
@@ -282,6 +265,8 @@ pub async fn downloads_get(
|
||||
/// }
|
||||
///}
|
||||
/// ONLY project IDs can be used. Unauthorized projects will be filtered out.
|
||||
#[utoipa::path]
|
||||
#[get("/revenue")]
|
||||
pub async fn revenue_get(
|
||||
req: HttpRequest,
|
||||
data: web::Query<GetData>,
|
||||
@@ -409,7 +394,8 @@ pub async fn revenue_get(
|
||||
Ok(HttpResponse::Ok().json(hm))
|
||||
}
|
||||
|
||||
/// Get country data for a set of projects or versions
|
||||
/// Get country data for a set of projects or versions.
|
||||
///
|
||||
/// Data is returned as a hashmap of project/version ids to a hashmap of coutnry to downloads.
|
||||
/// Unknown countries are labeled "".
|
||||
/// This is usable to see significant performing countries per project
|
||||
@@ -421,6 +407,8 @@ pub async fn revenue_get(
|
||||
///}
|
||||
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
|
||||
/// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch
|
||||
#[utoipa::path]
|
||||
#[get("/countries/downloads")]
|
||||
pub async fn countries_downloads_get(
|
||||
req: HttpRequest,
|
||||
clickhouse: web::Data<clickhouse::Client>,
|
||||
@@ -482,7 +470,8 @@ pub async fn countries_downloads_get(
|
||||
Ok(HttpResponse::Ok().json(hm))
|
||||
}
|
||||
|
||||
/// Get country data for a set of projects or versions
|
||||
/// Get country data for a set of projects or versions.
|
||||
///
|
||||
/// Data is returned as a hashmap of project/version ids to a hashmap of coutnry to views.
|
||||
/// Unknown countries are labeled "".
|
||||
/// This is usable to see significant performing countries per project
|
||||
@@ -494,6 +483,8 @@ pub async fn countries_downloads_get(
|
||||
///}
|
||||
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
|
||||
/// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch
|
||||
#[utoipa::path]
|
||||
#[get("/countries/views")]
|
||||
pub async fn countries_views_get(
|
||||
req: HttpRequest,
|
||||
clickhouse: web::Data<clickhouse::Client>,
|
||||
@@ -4,7 +4,6 @@ use actix_web::{HttpResponse, web};
|
||||
use serde_json::json;
|
||||
|
||||
pub mod analytics_get;
|
||||
pub mod analytics_get_old;
|
||||
pub mod collections;
|
||||
pub mod friends;
|
||||
pub mod images;
|
||||
@@ -33,8 +32,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
web::scope("v3")
|
||||
.wrap(default_cors())
|
||||
.configure(limits::config)
|
||||
// .configure(analytics_get::config) // TODO: see `analytics_get`
|
||||
.configure(analytics_get_old::config)
|
||||
.configure(collections::config)
|
||||
.configure(images::config)
|
||||
.configure(notifications::config)
|
||||
@@ -56,6 +53,15 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn utoipa_config(
|
||||
cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
|
||||
) {
|
||||
cfg.service(
|
||||
utoipa_actix_web::scope("/v3/analytics")
|
||||
.configure(analytics_get::config),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn hello_world() -> Result<HttpResponse, ApiError> {
|
||||
Ok(HttpResponse::Ok().json(json!({
|
||||
"hello": "world",
|
||||
|
||||
@@ -6,6 +6,7 @@ use actix_web::{App, dev::ServiceResponse, test};
|
||||
use async_trait::async_trait;
|
||||
use labrinth::LabrinthConfig;
|
||||
use std::rc::Rc;
|
||||
use utoipa_actix_web::AppExt;
|
||||
|
||||
pub mod project;
|
||||
pub mod request_data;
|
||||
@@ -22,9 +23,15 @@ pub struct ApiV2 {
|
||||
#[async_trait(?Send)]
|
||||
impl ApiBuildable for ApiV2 {
|
||||
async fn build(labrinth_config: LabrinthConfig) -> Self {
|
||||
let app = App::new().configure(|cfg| {
|
||||
labrinth::app_config(cfg, labrinth_config.clone())
|
||||
});
|
||||
let app = App::new()
|
||||
.into_utoipa_app()
|
||||
.configure(|cfg| {
|
||||
labrinth::utoipa_app_config(cfg, labrinth_config.clone())
|
||||
})
|
||||
.into_app()
|
||||
.configure(|cfg| {
|
||||
labrinth::app_config(cfg, labrinth_config.clone())
|
||||
});
|
||||
let test_app: Rc<dyn LocalService> =
|
||||
Rc::new(test::init_service(app).await);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use actix_web::{App, dev::ServiceResponse, test};
|
||||
use async_trait::async_trait;
|
||||
use labrinth::LabrinthConfig;
|
||||
use std::rc::Rc;
|
||||
use utoipa_actix_web::AppExt;
|
||||
|
||||
pub mod collections;
|
||||
pub mod limits;
|
||||
@@ -27,9 +28,15 @@ pub struct ApiV3 {
|
||||
#[async_trait(?Send)]
|
||||
impl ApiBuildable for ApiV3 {
|
||||
async fn build(labrinth_config: LabrinthConfig) -> Self {
|
||||
let app = App::new().configure(|cfg| {
|
||||
labrinth::app_config(cfg, labrinth_config.clone())
|
||||
});
|
||||
let app = App::new()
|
||||
.into_utoipa_app()
|
||||
.configure(|cfg| {
|
||||
labrinth::utoipa_app_config(cfg, labrinth_config.clone())
|
||||
})
|
||||
.into_app()
|
||||
.configure(|cfg| {
|
||||
labrinth::app_config(cfg, labrinth_config.clone())
|
||||
});
|
||||
let test_app: Rc<dyn LocalService> =
|
||||
Rc::new(test::init_service(app).await);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ serde_bytes = { workspace = true }
|
||||
serde_cbor = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
utoipa = { workspace = true }
|
||||
uuid = { workspace = true, features = ["fast-rng", "serde", "v4"] }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -94,6 +94,7 @@ macro_rules! base62_id {
|
||||
serde::Deserialize,
|
||||
Debug,
|
||||
Hash,
|
||||
utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(from = "ariadne::ids::Base62Id")]
|
||||
#[serde(into = "ariadne::ids::Base62Id")]
|
||||
|
||||
Reference in New Issue
Block a user