137 lines
4.2 KiB
Rust
137 lines
4.2 KiB
Rust
use crate::{
|
|
errors::ApiError, extractors::OptionalAuthUser, handlers::feed::to_thought_response,
|
|
state::AppState,
|
|
};
|
|
use api_types::requests::PaginationQuery;
|
|
use application::use_cases::feed::get_user_feed;
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
Json,
|
|
};
|
|
use domain::{events::DomainEvent, models::feed::PageParams};
|
|
|
|
pub async fn remote_actor_posts_handler(
|
|
State(s): State<AppState>,
|
|
Path(handle): Path<String>,
|
|
Query(q): Query<PaginationQuery>,
|
|
OptionalAuthUser(viewer): OptionalAuthUser,
|
|
) -> Result<Json<serde_json::Value>, ApiError> {
|
|
let actor = s.federation.lookup_actor(&handle).await?;
|
|
|
|
let ap_url = url::Url::parse(&actor.url).map_err(|e| ApiError::BadRequest(e.to_string()))?;
|
|
|
|
// Get or create interned local UserId for this remote actor
|
|
let author_id = match s.ap_repo.find_remote_actor_id(&ap_url).await? {
|
|
Some(id) => id,
|
|
None => s.ap_repo.intern_remote_actor(&ap_url).await?,
|
|
};
|
|
|
|
// Return cached posts from DB
|
|
let page = PageParams {
|
|
page: q.page(),
|
|
per_page: q.per_page(),
|
|
};
|
|
let result = get_user_feed(&*s.feed, &author_id, page, viewer.as_ref()).await?;
|
|
|
|
// Trigger background outbox fetch (fire and forget)
|
|
if let Some(outbox_url) = &actor.outbox_url {
|
|
let _ = s
|
|
.events
|
|
.publish(&DomainEvent::FetchRemoteActorPosts {
|
|
actor_ap_url: actor.url.clone(),
|
|
outbox_url: outbox_url.clone(),
|
|
})
|
|
.await;
|
|
}
|
|
|
|
Ok(Json(serde_json::json!({
|
|
"total": result.total,
|
|
"page": result.page,
|
|
"per_page": result.per_page,
|
|
"items": result.items.iter().map(to_thought_response).collect::<Vec<_>>(),
|
|
})))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use async_trait::async_trait;
|
|
use axum::{body::Body, http::Request, routing::get, Router};
|
|
use domain::{
|
|
errors::DomainError,
|
|
ports::{AuthService, GeneratedToken, PasswordHasher},
|
|
testing::TestStore,
|
|
value_objects::{PasswordHash, UserId},
|
|
};
|
|
use std::sync::Arc;
|
|
use tower::ServiceExt;
|
|
|
|
struct NoOpAuth;
|
|
impl AuthService for NoOpAuth {
|
|
fn generate_token(&self, _uid: &UserId) -> Result<GeneratedToken, DomainError> {
|
|
Err(DomainError::Internal("noop".into()))
|
|
}
|
|
fn validate_token(&self, _token: &str) -> Result<UserId, DomainError> {
|
|
Err(DomainError::Unauthorized)
|
|
}
|
|
}
|
|
|
|
struct NoOpHasher;
|
|
#[async_trait]
|
|
impl PasswordHasher for NoOpHasher {
|
|
async fn hash(&self, _plain: &str) -> Result<PasswordHash, DomainError> {
|
|
Err(DomainError::Internal("noop".into()))
|
|
}
|
|
async fn verify(&self, _plain: &str, _hash: &PasswordHash) -> Result<bool, DomainError> {
|
|
Ok(false)
|
|
}
|
|
}
|
|
|
|
fn make_state() -> crate::state::AppState {
|
|
let store = Arc::new(TestStore::default());
|
|
crate::state::AppState {
|
|
users: store.clone(),
|
|
thoughts: store.clone(),
|
|
likes: store.clone(),
|
|
boosts: store.clone(),
|
|
follows: store.clone(),
|
|
blocks: store.clone(),
|
|
tags: store.clone(),
|
|
api_keys: store.clone(),
|
|
top_friends: store.clone(),
|
|
notifications: store.clone(),
|
|
remote_actors: store.clone(),
|
|
feed: store.clone(),
|
|
search: store.clone(),
|
|
auth: Arc::new(NoOpAuth),
|
|
hasher: Arc::new(NoOpHasher),
|
|
events: store.clone(),
|
|
federation: store.clone(),
|
|
ap_repo: store.clone(),
|
|
}
|
|
}
|
|
|
|
fn app() -> Router {
|
|
Router::new()
|
|
.route(
|
|
"/federation/actors/{handle}/posts",
|
|
get(remote_actor_posts_handler),
|
|
)
|
|
.with_state(make_state())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unknown_actor_returns_404() {
|
|
let resp = app()
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/federation/actors/%40alice%40example.com/posts")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), 404);
|
|
}
|
|
}
|