fix: content negotiation for followers/following — resolve AP router conflict
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 9m28s
test / unit (pull_request) Failing after 11m39s
test / integration (pull_request) Failing after 17m5s
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 9m28s
test / unit (pull_request) Failing after 11m39s
test / integration (pull_request) Failing after 17m5s
This commit is contained in:
@@ -1415,6 +1415,102 @@ impl domain::ports::FederationActionPort for ActivityPubService {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))
|
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn followers_collection_json(
|
||||||
|
&self,
|
||||||
|
user_id: &domain::value_objects::UserId,
|
||||||
|
page: Option<u32>,
|
||||||
|
) -> Result<String, domain::errors::DomainError> {
|
||||||
|
let data = self.federation_config.to_request_data();
|
||||||
|
let uuid = user_id.as_uuid();
|
||||||
|
let collection_id = format!("{}/users/{}/followers", self.base_url, uuid);
|
||||||
|
let total = data
|
||||||
|
.federation_repo
|
||||||
|
.count_followers(uuid)
|
||||||
|
.await
|
||||||
|
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))?;
|
||||||
|
let obj = if let Some(p) = page {
|
||||||
|
let p = p.max(1);
|
||||||
|
let offset = (p.saturating_sub(1) as usize) * 20;
|
||||||
|
let followers = data
|
||||||
|
.federation_repo
|
||||||
|
.get_followers_page(uuid, offset as u32, 20)
|
||||||
|
.await
|
||||||
|
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))?;
|
||||||
|
let has_next = offset + followers.len() < total;
|
||||||
|
let items: Vec<String> = followers.into_iter().map(|f| f.actor.url).collect();
|
||||||
|
let mut obj = serde_json::json!({
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"type": "OrderedCollectionPage",
|
||||||
|
"id": format!("{}?page={}", collection_id, p),
|
||||||
|
"partOf": collection_id,
|
||||||
|
"totalItems": total,
|
||||||
|
"orderedItems": items,
|
||||||
|
});
|
||||||
|
if has_next {
|
||||||
|
obj["next"] = serde_json::json!(format!("{}?page={}", collection_id, p + 1));
|
||||||
|
}
|
||||||
|
obj
|
||||||
|
} else {
|
||||||
|
serde_json::json!({
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"type": "OrderedCollection",
|
||||||
|
"id": collection_id,
|
||||||
|
"totalItems": total,
|
||||||
|
"first": format!("{}?page=1", collection_id),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
serde_json::to_string(&obj)
|
||||||
|
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn following_collection_json(
|
||||||
|
&self,
|
||||||
|
user_id: &domain::value_objects::UserId,
|
||||||
|
page: Option<u32>,
|
||||||
|
) -> Result<String, domain::errors::DomainError> {
|
||||||
|
let data = self.federation_config.to_request_data();
|
||||||
|
let uuid = user_id.as_uuid();
|
||||||
|
let collection_id = format!("{}/users/{}/following", self.base_url, uuid);
|
||||||
|
let total = data
|
||||||
|
.federation_repo
|
||||||
|
.count_following(uuid)
|
||||||
|
.await
|
||||||
|
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))?;
|
||||||
|
let obj = if let Some(p) = page {
|
||||||
|
let p = p.max(1);
|
||||||
|
let offset = (p.saturating_sub(1) as usize) * 20;
|
||||||
|
let following = data
|
||||||
|
.federation_repo
|
||||||
|
.get_following_page(uuid, offset as u32, 20)
|
||||||
|
.await
|
||||||
|
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))?;
|
||||||
|
let has_next = offset + following.len() < total;
|
||||||
|
let items: Vec<String> = following.into_iter().map(|a| a.url).collect();
|
||||||
|
let mut obj = serde_json::json!({
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"type": "OrderedCollectionPage",
|
||||||
|
"id": format!("{}?page={}", collection_id, p),
|
||||||
|
"partOf": collection_id,
|
||||||
|
"totalItems": total,
|
||||||
|
"orderedItems": items,
|
||||||
|
});
|
||||||
|
if has_next {
|
||||||
|
obj["next"] = serde_json::json!(format!("{}?page={}", collection_id, p + 1));
|
||||||
|
}
|
||||||
|
obj
|
||||||
|
} else {
|
||||||
|
serde_json::json!({
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"type": "OrderedCollection",
|
||||||
|
"id": collection_id,
|
||||||
|
"totalItems": total,
|
||||||
|
"first": format!("{}?page=1", collection_id),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
serde_json::to_string(&obj)
|
||||||
|
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ mod config;
|
|||||||
mod factory;
|
mod factory;
|
||||||
|
|
||||||
use activitypub_base::{
|
use activitypub_base::{
|
||||||
followers_handler::{followers_handler, following_handler},
|
|
||||||
inbox::inbox_handler,
|
inbox::inbox_handler,
|
||||||
nodeinfo::{nodeinfo_handler, nodeinfo_well_known_handler},
|
nodeinfo::{nodeinfo_handler, nodeinfo_well_known_handler},
|
||||||
outbox::outbox_handler,
|
outbox::outbox_handler,
|
||||||
@@ -57,14 +56,6 @@ async fn main() {
|
|||||||
"/users/{username}/outbox",
|
"/users/{username}/outbox",
|
||||||
axum::routing::get(outbox_handler),
|
axum::routing::get(outbox_handler),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/users/{username}/followers",
|
|
||||||
axum::routing::get(followers_handler),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/users/{username}/following",
|
|
||||||
axum::routing::get(following_handler),
|
|
||||||
)
|
|
||||||
.layer(infra.ap_service.federation_config().middleware());
|
.layer(infra.ap_service.federation_config().middleware());
|
||||||
|
|
||||||
let base = presentation::routes::router()
|
let base = presentation::routes::router()
|
||||||
|
|||||||
@@ -199,6 +199,16 @@ pub trait FederationActionPort: Send + Sync {
|
|||||||
async fn lookup_actor(&self, handle: &str) -> Result<RemoteActor, DomainError>;
|
async fn lookup_actor(&self, handle: &str) -> Result<RemoteActor, DomainError>;
|
||||||
async fn follow_remote(&self, local_user_id: &UserId, handle: &str) -> Result<(), DomainError>;
|
async fn follow_remote(&self, local_user_id: &UserId, handle: &str) -> Result<(), DomainError>;
|
||||||
async fn actor_json(&self, user_id: &UserId) -> Result<String, DomainError>;
|
async fn actor_json(&self, user_id: &UserId) -> Result<String, DomainError>;
|
||||||
|
async fn followers_collection_json(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
page: Option<u32>,
|
||||||
|
) -> Result<String, DomainError>;
|
||||||
|
async fn following_collection_json(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
page: Option<u32>,
|
||||||
|
) -> Result<String, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -551,6 +551,22 @@ impl FederationActionPort for TestStore {
|
|||||||
async fn actor_json(&self, _user_id: &UserId) -> Result<String, DomainError> {
|
async fn actor_json(&self, _user_id: &UserId) -> Result<String, DomainError> {
|
||||||
Err(DomainError::NotFound)
|
Err(DomainError::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn followers_collection_json(
|
||||||
|
&self,
|
||||||
|
_user_id: &UserId,
|
||||||
|
_page: Option<u32>,
|
||||||
|
) -> Result<String, DomainError> {
|
||||||
|
Err(DomainError::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn following_collection_json(
|
||||||
|
&self,
|
||||||
|
_user_id: &UserId,
|
||||||
|
_page: Option<u32>,
|
||||||
|
) -> Result<String, DomainError> {
|
||||||
|
Err(DomainError::NotFound)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -14,9 +14,12 @@ use application::use_cases::profile::get_user_by_username;
|
|||||||
use application::use_cases::search::{search_thoughts, search_users};
|
use application::use_cases::search::{search_thoughts, search_users};
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
|
http::{header, HeaderMap},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use domain::models::feed::PageParams;
|
use domain::models::feed::PageParams;
|
||||||
|
use domain::value_objects::UserId;
|
||||||
|
|
||||||
fn to_thought_response(e: &domain::models::feed::FeedEntry) -> ThoughtResponse {
|
fn to_thought_response(e: &domain::models::feed::FeedEntry) -> ThoughtResponse {
|
||||||
ThoughtResponse {
|
ThoughtResponse {
|
||||||
@@ -151,34 +154,82 @@ pub async fn search_handler(
|
|||||||
|
|
||||||
pub async fn get_following_handler(
|
pub async fn get_following_handler(
|
||||||
State(s): State<AppState>,
|
State(s): State<AppState>,
|
||||||
Path(username): Path<String>,
|
Path(param): Path<String>,
|
||||||
Query(q): Query<PaginationQuery>,
|
Query(q): Query<PaginationQuery>,
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
headers: HeaderMap,
|
||||||
let user = get_user_by_username(&*s.users, &username).await?;
|
) -> Result<Response, ApiError> {
|
||||||
|
let accept = headers
|
||||||
|
.get(header::ACCEPT)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
if accept.contains("application/activity+json") {
|
||||||
|
let user_id = resolve_user_id(&s, ¶m).await?;
|
||||||
|
let page = q.page().try_into().ok();
|
||||||
|
let json = s
|
||||||
|
.federation
|
||||||
|
.following_collection_json(&user_id, page)
|
||||||
|
.await?;
|
||||||
|
return Ok(([(header::CONTENT_TYPE, "application/activity+json")], json).into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = get_user_by_username(&*s.users, ¶m).await?;
|
||||||
let page = PageParams {
|
let page = PageParams {
|
||||||
page: q.page(),
|
page: q.page(),
|
||||||
per_page: q.per_page(),
|
per_page: q.per_page(),
|
||||||
};
|
};
|
||||||
let result = get_following(&*s.follows, &user.id, page).await?;
|
let result = get_following(&*s.follows, &user.id, page).await?;
|
||||||
Ok(Json(
|
Ok(Json(serde_json::json!({
|
||||||
serde_json::json!({ "total": result.total, "items": result.items.iter().map(to_user_response).collect::<Vec<_>>() }),
|
"total": result.total,
|
||||||
))
|
"items": result.items.iter().map(to_user_response).collect::<Vec<_>>()
|
||||||
|
}))
|
||||||
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_followers_handler(
|
pub async fn get_followers_handler(
|
||||||
State(s): State<AppState>,
|
State(s): State<AppState>,
|
||||||
Path(username): Path<String>,
|
Path(param): Path<String>,
|
||||||
Query(q): Query<PaginationQuery>,
|
Query(q): Query<PaginationQuery>,
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
headers: HeaderMap,
|
||||||
let user = get_user_by_username(&*s.users, &username).await?;
|
) -> Result<Response, ApiError> {
|
||||||
|
let accept = headers
|
||||||
|
.get(header::ACCEPT)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
if accept.contains("application/activity+json") {
|
||||||
|
let user_id = resolve_user_id(&s, ¶m).await?;
|
||||||
|
let page = q.page().try_into().ok();
|
||||||
|
let json = s
|
||||||
|
.federation
|
||||||
|
.followers_collection_json(&user_id, page)
|
||||||
|
.await?;
|
||||||
|
return Ok(([(header::CONTENT_TYPE, "application/activity+json")], json).into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = get_user_by_username(&*s.users, ¶m).await?;
|
||||||
let page = PageParams {
|
let page = PageParams {
|
||||||
page: q.page(),
|
page: q.page(),
|
||||||
per_page: q.per_page(),
|
per_page: q.per_page(),
|
||||||
};
|
};
|
||||||
let result = get_followers(&*s.follows, &user.id, page).await?;
|
let result = get_followers(&*s.follows, &user.id, page).await?;
|
||||||
Ok(Json(
|
Ok(Json(serde_json::json!({
|
||||||
serde_json::json!({ "total": result.total, "items": result.items.iter().map(to_user_response).collect::<Vec<_>>() }),
|
"total": result.total,
|
||||||
))
|
"items": result.items.iter().map(to_user_response).collect::<Vec<_>>()
|
||||||
|
}))
|
||||||
|
.into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_user_id(s: &AppState, param: &str) -> Result<UserId, ApiError> {
|
||||||
|
if let Ok(uuid) = uuid::Uuid::parse_str(param) {
|
||||||
|
s.users
|
||||||
|
.find_by_id(&UserId::from_uuid(uuid))
|
||||||
|
.await?
|
||||||
|
.map(|u| u.id)
|
||||||
|
.ok_or_else(|| ApiError::from(domain::errors::DomainError::NotFound))
|
||||||
|
} else {
|
||||||
|
Ok(get_user_by_username(&*s.users, param).await?.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
|
|||||||
Reference in New Issue
Block a user