Skip to content

Commit

Permalink
refactor(api_apub): update openapi docs (#71)
Browse files Browse the repository at this point in the history
* refactor(api_apub): add notice openapi docs

* refactor(api_apub): add user_* openapi docs

* refactor(apub): split structs

* refactor(api_apub): strict user_* types

* fix(apub/collection): fix total pages
  • Loading branch information
kwaa authored Sep 28, 2024
1 parent a33a262 commit f552794
Show file tree
Hide file tree
Showing 13 changed files with 266 additions and 152 deletions.
11 changes: 11 additions & 0 deletions crates/api_apub/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use hatsu_apub::{
actors::{PublicKeySchema, User, UserAttachment, UserImage},
collections::{Collection, CollectionOrPage, CollectionPage},
links::{Emoji, EmojiIcon, Hashtag, Mention, Tag},
objects::Note,
};
use serde_json::Value;
use url::Url;
use utoipa::OpenApi;
use utoipa_axum::router::OpenApiRouter;

Expand All @@ -14,11 +17,19 @@ pub const TAG: &str = "apub";

#[derive(OpenApi)]
#[openapi(
paths(
posts::notice::notice,
posts::post::post,
),
components(schemas(
PublicKeySchema,
User,
UserAttachment,
UserImage,
Collection,
CollectionOrPage,
CollectionPage<Url>,
CollectionPage<Value>,
Emoji,
EmojiIcon,
Hashtag,
Expand Down
3 changes: 1 addition & 2 deletions crates/api_apub/src/posts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ use utoipa_axum::router::OpenApiRouter;

use crate::ApubApi;

mod notice;
pub mod notice;
pub mod post;

pub fn routes() -> OpenApiRouter {
OpenApiRouter::with_openapi(ApubApi::openapi())
// TODO: writing utoipa docs
.route("/notice/*notice", get(notice::notice))
.route("/posts/*post", get(post::post))
.route("/p/*post", get(post::redirect))
Expand Down
15 changes: 15 additions & 0 deletions crates/api_apub/src/posts/notice.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
use axum::{debug_handler, extract::Path, response::Redirect};
use hatsu_utils::AppError;

use crate::TAG;

/// Get post by base64 url
#[utoipa::path(
get,
tag = TAG,
path = "/notice/{notice}",
responses(
(status = OK, description = "Post", body = Note),
(status = NOT_FOUND, description = "Post does not exist", body = AppError)
),
params(
("notice" = String, Path, description = "Base64 Post Url")
)
)]
#[debug_handler]
pub async fn notice(Path(base64_url): Path<String>) -> Result<Redirect, AppError> {
let base64 = base64_simd::URL_SAFE;
Expand Down
17 changes: 11 additions & 6 deletions crates/api_apub/src/users/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use axum::routing::{get, post};
use utoipa::OpenApi;
use serde::Deserialize;
use utoipa::{IntoParams, OpenApi};
use utoipa_axum::{router::OpenApiRouter, routes};

use crate::ApubApi;
Expand All @@ -9,14 +10,18 @@ mod user_following;
mod user_inbox;
mod user_outbox;

#[derive(Deserialize, IntoParams)]
pub struct Pagination {
page: Option<u64>,
}

pub fn routes() -> OpenApiRouter {
OpenApiRouter::with_openapi(ApubApi::openapi())
.routes(routes!(user::handler))
// TODO: writing utoipa docs
.route("/users/:user/followers", get(user_followers::handler))
.route("/users/:user/following", get(user_following::handler))
.route("/users/:user/outbox", get(user_outbox::handler))
.route("/users/:user/inbox", post(user_inbox::handler))
.routes(routes!(user_followers::handler))
.routes(routes!(user_following::handler))
.routes(routes!(user_inbox::handler))
.routes(routes!(user_outbox::handler))
// fallback routes
.route("/u/:user", get(user::redirect))
.route("/u/:user/followers", get(user_followers::redirect))
Expand Down
39 changes: 23 additions & 16 deletions crates/api_apub/src/users/user_followers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,35 @@ use axum::{
};
use hatsu_apub::{
actors::ApubUser,
collections::{Collection, CollectionPage},
collections::{Collection, CollectionOrPage, CollectionPage},
};
use hatsu_db_schema::{prelude::ReceivedFollow, received_follow};
use hatsu_utils::{AppData, AppError};
use sea_orm::{ModelTrait, PaginatorTrait, QueryOrder};
use serde::Deserialize;
use serde_json::Value;
use url::Url;

#[derive(Default, Deserialize)]
pub struct Pagination {
page: Option<u64>,
}
use crate::{users::Pagination, TAG};

/// Get user followers
#[utoipa::path(
get,
tag = TAG,
path = "/users/{user}/followers",
responses(
(status = OK, description = "Followers", body = CollectionOrPage),
(status = NOT_FOUND, description = "User does not exist", body = AppError)
),
params(
("user" = String, Path, description = "The Domain of the User in the database."),
Pagination
)
)]
#[debug_handler]
pub async fn handler(
Path(name): Path<String>,
pagination: Option<Query<Pagination>>,
pagination: Query<Pagination>,
data: Data<AppData>,
) -> Result<FederationJson<WithContext<Value>>, AppError> {
let Query(pagination) = pagination.unwrap_or_default();

) -> Result<FederationJson<WithContext<CollectionOrPage>>, AppError> {
let user_id: ObjectId<ApubUser> =
hatsu_utils::url::generate_user_url(data.domain(), &name)?.into();
let user = user_id.dereference_local(&data).await?;
Expand All @@ -48,12 +55,12 @@ pub async fn handler(

match pagination.page {
None => Ok(FederationJson(WithContext::new_default(
serde_json::to_value(Collection::new(
CollectionOrPage::Collection(Collection::new(
&hatsu_utils::url::generate_user_url(data.domain(), &name)?
.join(&format!("{name}/followers"))?,
total.number_of_items,
Some(total.number_of_pages),
)?)?,
total.number_of_pages,
)?),
))),
Some(page) =>
if page > 1 && page > total.number_of_pages {
Expand All @@ -63,7 +70,7 @@ pub async fn handler(
))
} else {
Ok(FederationJson(WithContext::new_default(
serde_json::to_value(CollectionPage::<Url>::new(
CollectionOrPage::CollectionPageUrl(CollectionPage::<Url>::new(
hatsu_utils::url::generate_user_url(data.domain(), &name)?
.join(&format!("{name}/followers"))?,
total.number_of_items,
Expand All @@ -76,7 +83,7 @@ pub async fn handler(
.collect(),
total.number_of_pages,
page,
)?)?,
)?),
)))
},
}
Expand Down
39 changes: 23 additions & 16 deletions crates/api_apub/src/users/user_following.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,50 @@ use axum::{
extract::{Path, Query},
response::Redirect,
};
use hatsu_apub::collections::{Collection, CollectionPage};
use hatsu_apub::collections::{Collection, CollectionOrPage, CollectionPage};
use hatsu_utils::{AppData, AppError};
use serde::Deserialize;
use serde_json::Value;
use url::Url;

#[derive(Default, Deserialize)]
pub struct Pagination {
page: Option<u64>,
}
use crate::{users::Pagination, TAG};

/// Get user following
#[utoipa::path(
get,
tag = TAG,
path = "/users/{user}/following",
responses(
(status = OK, description = "Following", body = CollectionOrPage),
(status = NOT_FOUND, description = "User does not exist", body = AppError)
),
params(
("user" = String, Path, description = "The Domain of the User in the database."),
Pagination
)
)]
#[debug_handler]
pub async fn handler(
Path(name): Path<String>,
pagination: Option<Query<Pagination>>,
pagination: Query<Pagination>,
data: Data<AppData>,
) -> Result<FederationJson<WithContext<Value>>, AppError> {
let Query(pagination) = pagination.unwrap_or_default();

) -> Result<FederationJson<WithContext<CollectionOrPage>>, AppError> {
match pagination.page {
None => Ok(FederationJson(WithContext::new_default(
serde_json::to_value(Collection::new(
CollectionOrPage::Collection(Collection::new(
&hatsu_utils::url::generate_user_url(data.domain(), &name)?
.join(&format!("{name}/following"))?,
0,
Some(0),
)?)?,
0,
)?),
))),
Some(page) => Ok(FederationJson(WithContext::new_default(
serde_json::to_value(CollectionPage::<Url>::new(
CollectionOrPage::CollectionPageUrl(CollectionPage::<Url>::new(
hatsu_utils::url::generate_user_url(data.domain(), &name)?
.join(&format!("{name}/following"))?,
0,
vec![],
0,
page,
)?)?,
)?),
))),
}
}
Expand Down
16 changes: 16 additions & 0 deletions crates/api_apub/src/users/user_inbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ use axum::{debug_handler, response::IntoResponse};
use hatsu_apub::{activities::UserInboxActivities, actors::ApubUser};
use hatsu_utils::AppData;

use crate::TAG;

/// User inbox
#[utoipa::path(
post,
tag = TAG,
path = "/users/{user}/inbox",
responses(
(status = OK),
(status = NOT_FOUND, body = AppError),
(status = INTERNAL_SERVER_ERROR, body = AppError)
),
params(
("user" = String, Path, description = "The Domain of the User in the database.")
)
)]
#[debug_handler]
pub async fn handler(data: Data<AppData>, activity_data: ActivityData) -> impl IntoResponse {
receive_activity::<WithContext<UserInboxActivities>, ApubUser, AppData>(activity_data, &data)
Expand Down
38 changes: 23 additions & 15 deletions crates/api_apub/src/users/user_outbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,35 @@ use axum::{
use hatsu_apub::{
activities::ApubActivity,
actors::ApubUser,
collections::{Collection, CollectionPage},
collections::{Collection, CollectionOrPage, CollectionPage},
};
use hatsu_db_schema::{activity, prelude::Activity};
use hatsu_utils::{AppData, AppError};
use sea_orm::{ColumnTrait, ModelTrait, PaginatorTrait, QueryFilter, QueryOrder};
use serde::Deserialize;
use serde_json::Value;

#[derive(Default, Deserialize)]
pub struct Pagination {
page: Option<u64>,
}
use crate::{users::Pagination, TAG};

/// Get user outbox
#[utoipa::path(
get,
tag = TAG,
path = "/users/{user}/outbox",
responses(
(status = OK, description = "Outbox", body = CollectionOrPage),
(status = NOT_FOUND, description = "User does not exist", body = AppError)
),
params(
("user" = String, Path, description = "The Domain of the User in the database."),
Pagination
)
)]
#[debug_handler]
pub async fn handler(
Path(name): Path<String>,
pagination: Option<Query<Pagination>>,
pagination: Query<Pagination>,
data: Data<AppData>,
) -> Result<FederationJson<WithContext<Value>>, AppError> {
let Query(pagination) = pagination.unwrap_or_default();

) -> Result<FederationJson<WithContext<CollectionOrPage>>, AppError> {
let user_id: ObjectId<ApubUser> =
hatsu_utils::url::generate_user_url(data.domain(), &name)?.into();
let user = user_id.dereference_local(&data).await?;
Expand All @@ -50,12 +58,12 @@ pub async fn handler(

match pagination.page {
None => Ok(FederationJson(WithContext::new_default(
serde_json::to_value(Collection::new(
CollectionOrPage::Collection(Collection::new(
&hatsu_utils::url::generate_user_url(data.domain(), &name)?
.join(&format!("{name}/outbox"))?,
total.number_of_items,
Some(total.number_of_pages),
)?)?,
total.number_of_pages,
)?),
))),
Some(page) =>
if page > 1 && page > total.number_of_pages {
Expand All @@ -65,7 +73,7 @@ pub async fn handler(
))
} else {
Ok(FederationJson(WithContext::new_default(
serde_json::to_value(CollectionPage::<Value>::new(
CollectionOrPage::CollectionPageValue(CollectionPage::<Value>::new(
hatsu_utils::url::generate_user_url(data.domain(), &name)?
.join(&format!("{name}/outbox"))?,
total.number_of_items,
Expand All @@ -81,7 +89,7 @@ pub async fn handler(
.collect(),
total.number_of_pages,
page,
)?)?,
)?),
)))
},
}
Expand Down
37 changes: 37 additions & 0 deletions crates/apub/src/collections/collection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use activitypub_federation::kinds::collection::OrderedCollectionType;
use hatsu_utils::AppError;
use serde::{Deserialize, Serialize};
use url::Url;
use utoipa::ToSchema;

use crate::collections::generate_collection_page_url;

#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Collection {
#[serde(rename = "type")]
pub kind: OrderedCollectionType,
// example: https://hatsu.local/users/example.com/collection
pub id: Url,
// example: https://hatsu.local/users/example.com/collection?page=1
pub first: Url,
// example: https://hatsu.local/users/example.com/collection?page=64
pub last: Url,
// collection count
pub total_items: u64,
}

impl Collection {
pub fn new(collection_id: &Url, total_items: u64, total_pages: u64) -> Result<Self, AppError> {
Ok(Self {
kind: OrderedCollectionType::OrderedCollection,
id: collection_id.clone(),
first: generate_collection_page_url(collection_id, 1)?,
last: generate_collection_page_url(collection_id, match total_pages {
page if page > 0 => page,
_ => 1,
})?,
total_items,
})
}
}
Loading

0 comments on commit f552794

Please sign in to comment.