Try using utoipa for list posts endpoint (ref #2937)

This commit is contained in:
Felix Ableitner 2023-06-28 14:44:47 +02:00
parent e4b739320c
commit 4869b16bd8
9 changed files with 137 additions and 61 deletions

35
Cargo.lock generated
View file

@ -2218,9 +2218,9 @@ checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29"
[[package]]
name = "http-signature-normalization"
version = "0.6.0"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8f45adbef81d7ea3bd7e9bcc6734b7245dad05a14abdcc7ddc0988791d63515"
checksum = "8b93438e69bb70b5c01d144da52b9e17505d84b9b84a6dc70d2750365df9ca6e"
dependencies = [
"httpdate",
]
@ -2245,7 +2245,7 @@ dependencies = [
"actix-web",
"base64 0.13.1",
"futures-util",
"http-signature-normalization 0.6.0",
"http-signature-normalization 0.6.1",
"sha2",
"thiserror",
"tokio",
@ -2617,6 +2617,7 @@ dependencies = [
"tracing",
"ts-rs",
"url",
"utoipa",
"uuid",
"webpage",
]
@ -2677,6 +2678,7 @@ dependencies = [
"tokio",
"tracing",
"url",
"utoipa",
"uuid",
]
@ -2824,6 +2826,7 @@ dependencies = [
"tracing-opentelemetry 0.17.4",
"tracing-subscriber",
"url",
"utoipa",
]
[[package]]
@ -6104,6 +6107,32 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
[[package]]
name = "utoipa"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ae74ef183fae36d650f063ae7bde1cacbe1cd7e72b617cbe1e985551878b98"
dependencies = [
"indexmap",
"serde",
"serde_json",
"utoipa-gen",
]
[[package]]
name = "utoipa-gen"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ea8ac818da7e746a63285594cce8a96f5e00ee31994e655bd827569cb8b137b"
dependencies = [
"lazy_static",
"proc-macro-error",
"proc-macro2",
"quote",
"regex",
"syn 2.0.18",
]
[[package]]
name = "uuid"
version = "1.3.4"

View file

@ -110,6 +110,7 @@ rustls = { version ="0.21.2", features = ["dangerous_configuration"]}
futures-util = "0.3.28"
tokio-postgres = "0.7.8"
tokio-postgres-rustls = "0.10.0"
utoipa = { version = "3", features = ["actix_extras"] }
[dependencies]
lemmy_api = { workspace = true }
@ -147,4 +148,5 @@ rustls = { workspace = true }
futures-util = { workspace = true }
tokio-postgres = { workspace = true }
tokio-postgres-rustls = { workspace = true }
chrono = { workspace = true }
chrono = { workspace = true }
utoipa = { workspace = true }

View file

@ -16,7 +16,7 @@ doctest = false
[features]
full = ["tracing", "rosetta-i18n", "chrono", "lemmy_utils",
"lemmy_db_views/full", "lemmy_db_views_actor/full", "lemmy_db_views_moderator/full",
"percent-encoding", "encoding", "reqwest-middleware", "webpage", "ts-rs"]
"percent-encoding", "encoding", "reqwest-middleware", "webpage", "ts-rs", "utoipa"]
[dependencies]
lemmy_db_views = { workspace = true }
@ -42,3 +42,4 @@ tokio = { workspace = true }
reqwest = { workspace = true }
ts-rs = { workspace = true, optional = true }
actix-web = { workspace = true }
utoipa = { workspace = true, optional = true }

View file

@ -9,9 +9,9 @@ use lemmy_db_views::structs::{PostReportView, PostView};
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
use ts_rs::TS;
use url::Url;
#[cfg(feature = "full")]
use {ts_rs::TS, utoipa::ToSchema};
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
@ -64,7 +64,7 @@ pub struct GetPostResponse {
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", derive(TS, ToSchema))]
#[cfg_attr(feature = "full", ts(export))]
/// Get a list of posts.
pub struct GetPosts {
@ -79,7 +79,7 @@ pub struct GetPosts {
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", derive(TS, ToSchema))]
#[cfg_attr(feature = "full", ts(export))]
/// The post list response.
pub struct GetPostsResponse {

View file

@ -38,8 +38,9 @@ async-trait = { workspace = true }
anyhow = { workspace = true }
reqwest = { workspace = true }
once_cell = { workspace = true }
html2md = "0.2.14"
serde_with = { workspace = true }
utoipa = { workspace = true }
html2md = "0.2.14"
http-signature-normalization-actix = { version = "0.6.2", default-features = false, features = ["server", "sha-2"] }
enum_delegate = "0.2.0"

View file

@ -1,9 +1,13 @@
use crate::{
api::{listing_type_with_default, PerformApub},
api::listing_type_with_default,
fetcher::resolve_actor_identifier,
objects::community::ApubCommunity,
};
use activitypub_federation::config::Data;
use actix_web::{
get,
web::{Json, Query},
};
use lemmy_api_common::{
context::LemmyContext,
post::{GetPosts, GetPostsResponse},
@ -13,54 +17,62 @@ use lemmy_db_schema::source::{community::Community, local_site::LocalSite};
use lemmy_db_views::post_view::PostQuery;
use lemmy_utils::error::LemmyError;
#[async_trait::async_trait]
impl PerformApub for GetPosts {
type Response = GetPostsResponse;
// TODO: we need to duplicate the param/response structs here, and also in /src/lib.rs
// TODO: context_path workaround is needed because scopes arent supported, so another duplicated info
// https://github.com/juhaku/utoipa/issues/121
#[utoipa::path(
context_path = "/post",
request_body = GetPosts,
responses(
(status = 200, description = "List posts according to query parameters", body = GetPostsResponse)
)
)]
#[get("list")]
#[tracing::instrument(skip(context))]
// TODO: activitypub_federation::config::Data should impl actix_web::web::Data
pub async fn list_posts(
data: Query<GetPosts>,
context: Data<LemmyContext>,
) -> Result<Json<GetPostsResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
let local_site = LocalSite::read(context.pool()).await?;
#[tracing::instrument(skip(context))]
async fn perform(&self, context: &Data<LemmyContext>) -> Result<GetPostsResponse, LemmyError> {
let data: &GetPosts = self;
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), context).await;
let local_site = LocalSite::read(context.pool()).await?;
check_private_instance(&local_user_view, &local_site)?;
check_private_instance(&local_user_view, &local_site)?;
let sort = data.sort;
let sort = data.sort;
let page = data.page;
let limit = data.limit;
let community_id = if let Some(name) = &data.community_name {
resolve_actor_identifier::<ApubCommunity, Community>(name, context, &None, true)
.await
.ok()
.map(|c| c.id)
} else {
data.community_id
};
let saved_only = data.saved_only;
let listing_type = listing_type_with_default(data.type_, &local_site, community_id)?;
let is_mod_or_admin =
is_mod_or_admin_opt(context.pool(), local_user_view.as_ref(), community_id)
.await
.is_ok();
let posts = PostQuery::builder()
.pool(context.pool())
.local_user(local_user_view.map(|l| l.local_user).as_ref())
.listing_type(Some(listing_type))
.sort(sort)
.community_id(community_id)
.saved_only(saved_only)
.page(page)
.limit(limit)
.is_mod_or_admin(Some(is_mod_or_admin))
.build()
.list()
let page = data.page;
let limit = data.limit;
let community_id = if let Some(name) = &data.community_name {
resolve_actor_identifier::<ApubCommunity, Community>(name, &context, &None, true)
.await
.map_err(|e| LemmyError::from_error_message(e, "couldnt_get_posts"))?;
.ok()
.map(|c| c.id)
} else {
data.community_id
};
let saved_only = data.saved_only;
Ok(GetPostsResponse { posts })
}
let listing_type = listing_type_with_default(data.type_, &local_site, community_id)?;
let is_mod_or_admin = is_mod_or_admin_opt(context.pool(), local_user_view.as_ref(), community_id)
.await
.is_ok();
let posts = PostQuery::builder()
.pool(context.pool())
.local_user(local_user_view.map(|l| l.local_user).as_ref())
.listing_type(Some(listing_type))
.sort(sort)
.community_id(community_id)
.saved_only(saved_only)
.page(page)
.limit(limit)
.is_mod_or_admin(Some(is_mod_or_admin))
.build()
.list()
.await
.map_err(|e| LemmyError::from_error_message(e, "couldnt_get_posts"))?;
Ok(Json(GetPostsResponse { posts }))
}

View file

@ -4,7 +4,7 @@ use lemmy_db_schema::{newtypes::CommunityId, source::local_site::LocalSite, List
use lemmy_utils::error::LemmyError;
mod list_comments;
mod list_posts;
pub mod list_posts;
mod read_community;
mod read_person;
mod resolve_object;

View file

@ -62,7 +62,6 @@ use lemmy_api_common::{
EditPost,
FeaturePost,
GetPost,
GetPosts,
GetSiteMetadata,
ListPostReports,
LockPost,
@ -100,7 +99,10 @@ use lemmy_api_common::{
},
};
use lemmy_api_crud::PerformCrud;
use lemmy_apub::{api::PerformApub, SendActivity};
use lemmy_apub::{
api::{list_posts::list_posts, PerformApub},
SendActivity,
};
use lemmy_utils::rate_limit::RateLimitCell;
use serde::Deserialize;
@ -186,7 +188,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
)
.route("/lock", web::post().to(route_post::<LockPost>))
.route("/feature", web::post().to(route_post::<FeaturePost>))
.route("/list", web::get().to(route_get_apub::<GetPosts>))
.service(list_posts)
.route("/like", web::post().to(route_post::<CreatePostLike>))
.route("/save", web::put().to(route_post::<SavePost>))
.route("/report", web::post().to(route_post::<CreatePostReport>))

View file

@ -12,6 +12,7 @@ use actix_web::{middleware, web::Data, App, HttpServer, Result};
use lemmy_api_common::{
context::LemmyContext,
lemmy_db_views::structs::SiteView,
post::{GetPosts, GetPostsResponse},
request::build_user_agent,
utils::{
check_private_instance_and_federation_enabled,
@ -28,23 +29,51 @@ use lemmy_utils::{error::LemmyError, rate_limit::RateLimitCell, settings::SETTIN
use reqwest::Client;
use reqwest_middleware::ClientBuilder;
use reqwest_tracing::TracingMiddleware;
use std::{env, thread, time::Duration};
use std::{env, process::exit, thread, time::Duration};
use tracing::subscriber::set_global_default;
use tracing_actix_web::TracingLogger;
use tracing_error::ErrorLayer;
use tracing_log::LogTracer;
use tracing_subscriber::{filter::Targets, layer::SubscriberExt, Layer, Registry};
use url::Url;
use utoipa::{openapi::Server, Modify, OpenApi};
/// Max timeout for http requests
pub(crate) const REQWEST_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(OpenApi)]
#[openapi(modifiers(&ServerAddon))]
// TODO: components is incomplete, we need to manually include all nested response structs
#[openapi(
paths(lemmy_apub::api::list_posts::list_posts),
components(schemas(GetPosts, GetPostsResponse))
)]
struct ApiDoc;
struct ServerAddon;
impl Modify for ServerAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
// TODO: api prefix also needs to be duplicated
openapi.servers = Some(vec![Server::new("/api/v3")])
}
}
/// Placing the main function in lib.rs allows other crates to import it and embed Lemmy
pub async fn start_lemmy_server() -> Result<(), LemmyError> {
let args: Vec<String> = env::args().collect();
let scheduled_tasks_enabled = args.get(1) != Some(&"--disable-scheduled-tasks".to_string());
if args.get(1) == Some(&"--print-api-docs".to_string()) {
println!(
"{}",
ApiDoc::openapi()
.to_pretty_json()
.expect("print openapi json")
);
exit(0);
}
let settings = SETTINGS.to_owned();
// Run the DB migrations