federation refinement
This commit is contained in:
@@ -51,6 +51,7 @@ pub struct LoginForm {
|
||||
#[derive(Deserialize)]
|
||||
pub struct RegisterForm {
|
||||
pub email: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
@@ -131,6 +132,7 @@ pub struct LoginResponse {
|
||||
#[derive(Deserialize)]
|
||||
pub struct RegisterRequest {
|
||||
pub email: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
@@ -244,6 +246,11 @@ pub struct UnfollowForm {
|
||||
pub actor_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct FollowerActionForm {
|
||||
pub actor_url: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Default)]
|
||||
pub struct ProfileQueryParams {
|
||||
pub view: Option<String>,
|
||||
|
||||
@@ -142,6 +142,7 @@ mod tests {
|
||||
async fn find_by_email(&self, _: &Email) -> Result<Option<User>, DomainError> { panic!("unexpected") }
|
||||
async fn save(&self, _: &User) -> Result<(), DomainError> { panic!("unexpected") }
|
||||
async fn find_by_id(&self, _: &domain::value_objects::UserId) -> Result<Option<User>, DomainError> { panic!("unexpected") }
|
||||
async fn find_by_username(&self, _: &domain::value_objects::Username) -> Result<Option<User>, DomainError> { panic!("unexpected") }
|
||||
async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, DomainError> { panic!("unexpected") }
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ mod tests {
|
||||
#[async_trait::async_trait] impl domain::ports::EventPublisher for PanicEvent { async fn publish(&self, _: &domain::events::DomainEvent) -> Result<(), domain::errors::DomainError> { panic!() } }
|
||||
#[async_trait::async_trait] impl domain::ports::PasswordHasher for PanicHasher { async fn hash(&self, _: &str) -> Result<domain::value_objects::PasswordHash, domain::errors::DomainError> { panic!() } async fn verify(&self, _: &str, _: &domain::value_objects::PasswordHash) -> Result<bool, domain::errors::DomainError> { panic!() } }
|
||||
#[async_trait::async_trait] impl domain::ports::AuthService for PanicAuth { async fn generate_token(&self, _: &domain::value_objects::UserId) -> Result<domain::ports::GeneratedToken, domain::errors::DomainError> { panic!() } async fn validate_token(&self, _: &str) -> Result<domain::value_objects::UserId, domain::errors::DomainError> { panic!() } }
|
||||
#[async_trait::async_trait] impl domain::ports::UserRepository for PanicUserRepo { async fn find_by_email(&self, _: &domain::value_objects::Email) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn save(&self, _: &domain::models::User) -> Result<(), domain::errors::DomainError> { panic!() } async fn find_by_id(&self, _: &domain::value_objects::UserId) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, domain::errors::DomainError> { panic!() } }
|
||||
#[async_trait::async_trait] impl domain::ports::UserRepository for PanicUserRepo { async fn find_by_email(&self, _: &domain::value_objects::Email) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn save(&self, _: &domain::models::User) -> Result<(), domain::errors::DomainError> { panic!() } async fn find_by_id(&self, _: &domain::value_objects::UserId) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn find_by_username(&self, _: &domain::value_objects::Username) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, domain::errors::DomainError> { panic!() } }
|
||||
|
||||
let state = crate::state::AppState {
|
||||
app_ctx: AppContext {
|
||||
@@ -247,10 +247,28 @@ mod tests {
|
||||
async fn find_by_email(&self, _: &domain::value_objects::Email) -> Result<Option<domain::models::User>, domain::errors::DomainError> { Ok(None) }
|
||||
async fn save(&self, _: &domain::models::User) -> Result<(), domain::errors::DomainError> { Ok(()) }
|
||||
async fn find_by_id(&self, _: &domain::value_objects::UserId) -> Result<Option<domain::models::User>, domain::errors::DomainError> { Ok(None) }
|
||||
async fn find_by_username(&self, _: &domain::value_objects::Username) -> Result<Option<domain::models::User>, domain::errors::DomainError> { Ok(None) }
|
||||
async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, domain::errors::DomainError> { Ok(vec![]) }
|
||||
}
|
||||
struct DummyMovieRepo;
|
||||
#[async_trait::async_trait] impl domain::ports::MovieRepository for DummyMovieRepo {
|
||||
async fn get_movie_by_external_id(&self, _: &domain::value_objects::ExternalMetadataId) -> Result<Option<domain::models::Movie>, domain::errors::DomainError> { Ok(None) }
|
||||
async fn get_movie_by_id(&self, _: &domain::value_objects::MovieId) -> Result<Option<domain::models::Movie>, domain::errors::DomainError> { Ok(None) }
|
||||
async fn get_movies_by_title_and_year(&self, _: &domain::value_objects::MovieTitle, _: &domain::value_objects::ReleaseYear) -> Result<Vec<domain::models::Movie>, domain::errors::DomainError> { Ok(vec![]) }
|
||||
async fn upsert_movie(&self, _: &domain::models::Movie) -> Result<(), domain::errors::DomainError> { Ok(()) }
|
||||
async fn save_review(&self, _: &domain::models::Review) -> Result<domain::events::DomainEvent, domain::errors::DomainError> { panic!() }
|
||||
async fn query_diary(&self, _: &domain::models::DiaryFilter) -> Result<domain::models::collections::Paginated<domain::models::DiaryEntry>, domain::errors::DomainError> { panic!() }
|
||||
async fn get_review_history(&self, _: &domain::value_objects::MovieId) -> Result<domain::models::ReviewHistory, domain::errors::DomainError> { panic!() }
|
||||
async fn get_review_by_id(&self, _: &domain::value_objects::ReviewId) -> Result<Option<domain::models::Review>, domain::errors::DomainError> { Ok(None) }
|
||||
async fn delete_review(&self, _: &domain::value_objects::ReviewId) -> Result<(), domain::errors::DomainError> { Ok(()) }
|
||||
async fn delete_movie(&self, _: &domain::value_objects::MovieId) -> Result<(), domain::errors::DomainError> { Ok(()) }
|
||||
async fn query_activity_feed(&self, _: &domain::models::collections::PageParams) -> Result<domain::models::collections::Paginated<domain::models::FeedEntry>, domain::errors::DomainError> { panic!() }
|
||||
async fn get_user_stats(&self, _: &domain::value_objects::UserId) -> Result<domain::models::UserStats, domain::errors::DomainError> { panic!() }
|
||||
async fn get_user_history(&self, _: &domain::value_objects::UserId) -> Result<Vec<domain::models::DiaryEntry>, domain::errors::DomainError> { Ok(vec![]) }
|
||||
async fn get_user_trends(&self, _: &domain::value_objects::UserId) -> Result<domain::models::UserTrends, domain::errors::DomainError> { panic!() }
|
||||
}
|
||||
Arc::new(
|
||||
activitypub::ActivityPubService::new(fed_repo, Arc::new(DummyUserRepo), "http://localhost:3000".to_string(), true)
|
||||
activitypub::ActivityPubService::new(fed_repo, Arc::new(DummyUserRepo), Arc::new(DummyMovieRepo), "http://localhost:3000".to_string(), true)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -284,7 +302,7 @@ mod tests {
|
||||
#[async_trait::async_trait] impl domain::ports::EventPublisher for PanicEvent2 { async fn publish(&self, _: &domain::events::DomainEvent) -> Result<(), domain::errors::DomainError> { panic!() } }
|
||||
#[async_trait::async_trait] impl domain::ports::PasswordHasher for PanicHasher2 { async fn hash(&self, _: &str) -> Result<domain::value_objects::PasswordHash, domain::errors::DomainError> { panic!() } async fn verify(&self, _: &str, _: &domain::value_objects::PasswordHash) -> Result<bool, domain::errors::DomainError> { panic!() } }
|
||||
#[async_trait::async_trait] impl domain::ports::AuthService for PanicAuth2 { async fn generate_token(&self, _: &domain::value_objects::UserId) -> Result<domain::ports::GeneratedToken, domain::errors::DomainError> { panic!() } async fn validate_token(&self, _: &str) -> Result<domain::value_objects::UserId, domain::errors::DomainError> { panic!() } }
|
||||
#[async_trait::async_trait] impl domain::ports::UserRepository for PanicUserRepo2 { async fn find_by_email(&self, _: &domain::value_objects::Email) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn save(&self, _: &domain::models::User) -> Result<(), domain::errors::DomainError> { panic!() } async fn find_by_id(&self, _: &domain::value_objects::UserId) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, domain::errors::DomainError> { panic!() } }
|
||||
#[async_trait::async_trait] impl domain::ports::UserRepository for PanicUserRepo2 { async fn find_by_email(&self, _: &domain::value_objects::Email) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn save(&self, _: &domain::models::User) -> Result<(), domain::errors::DomainError> { panic!() } async fn find_by_id(&self, _: &domain::value_objects::UserId) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn find_by_username(&self, _: &domain::value_objects::Username) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, domain::errors::DomainError> { panic!() } }
|
||||
struct PanicRenderer2;
|
||||
impl crate::ports::HtmlRenderer for PanicRenderer2 {
|
||||
fn render_diary_page(&self, _: &domain::models::collections::Paginated<domain::models::DiaryEntry>, _: application::ports::HtmlPageContext) -> Result<String, String> { panic!() }
|
||||
@@ -346,7 +364,7 @@ mod tests {
|
||||
#[async_trait::async_trait] impl domain::ports::PosterStorage for PanicStorage3 { async fn store_poster(&self, _: &domain::value_objects::MovieId, _: &[u8]) -> Result<domain::value_objects::PosterPath, domain::errors::DomainError> { panic!() } async fn get_poster(&self, _: &domain::value_objects::PosterPath) -> Result<Vec<u8>, domain::errors::DomainError> { panic!() } }
|
||||
#[async_trait::async_trait] impl domain::ports::EventPublisher for PanicEvent3 { async fn publish(&self, _: &domain::events::DomainEvent) -> Result<(), domain::errors::DomainError> { panic!() } }
|
||||
#[async_trait::async_trait] impl domain::ports::PasswordHasher for PanicHasher3 { async fn hash(&self, _: &str) -> Result<domain::value_objects::PasswordHash, domain::errors::DomainError> { panic!() } async fn verify(&self, _: &str, _: &domain::value_objects::PasswordHash) -> Result<bool, domain::errors::DomainError> { panic!() } }
|
||||
#[async_trait::async_trait] impl domain::ports::UserRepository for PanicUserRepo3 { async fn find_by_email(&self, _: &domain::value_objects::Email) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn save(&self, _: &domain::models::User) -> Result<(), domain::errors::DomainError> { panic!() } async fn find_by_id(&self, _: &domain::value_objects::UserId) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, domain::errors::DomainError> { panic!() } }
|
||||
#[async_trait::async_trait] impl domain::ports::UserRepository for PanicUserRepo3 { async fn find_by_email(&self, _: &domain::value_objects::Email) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn save(&self, _: &domain::models::User) -> Result<(), domain::errors::DomainError> { panic!() } async fn find_by_id(&self, _: &domain::value_objects::UserId) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn find_by_username(&self, _: &domain::value_objects::Username) -> Result<Option<domain::models::User>, domain::errors::DomainError> { panic!() } async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, domain::errors::DomainError> { panic!() } }
|
||||
struct PanicRenderer3;
|
||||
impl crate::ports::HtmlRenderer for PanicRenderer3 {
|
||||
fn render_diary_page(&self, _: &domain::models::collections::Paginated<domain::models::DiaryEntry>, _: application::ports::HtmlPageContext) -> Result<String, String> { panic!() }
|
||||
|
||||
@@ -2,6 +2,8 @@ const DEFAULT_PAGE_LIMIT: u32 = 5;
|
||||
const RSS_FEED_LIMIT: u32 = 50;
|
||||
|
||||
pub mod html {
|
||||
use std::str::FromStr;
|
||||
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::{HeaderValue, StatusCode, header::SET_COOKIE},
|
||||
@@ -19,7 +21,7 @@ pub mod html {
|
||||
use domain::{errors::DomainError, value_objects::UserId};
|
||||
|
||||
use crate::{
|
||||
dtos::{DiaryQueryParams, ErrorQuery, FollowForm, LoginForm, LogReviewData, LogReviewForm, RegisterForm, UnfollowForm},
|
||||
dtos::{DiaryQueryParams, ErrorQuery, FollowForm, FollowerActionForm, LoginForm, LogReviewData, LogReviewForm, RegisterForm, UnfollowForm},
|
||||
extractors::{OptionalCookieUser, RequiredCookieUser},
|
||||
state::AppState,
|
||||
};
|
||||
@@ -153,6 +155,7 @@ pub mod html {
|
||||
&state.app_ctx,
|
||||
RegisterCommand {
|
||||
email: form.email,
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
},
|
||||
)
|
||||
@@ -294,10 +297,29 @@ pub mod html {
|
||||
OptionalCookieUser(user_id): OptionalCookieUser,
|
||||
State(state): State<AppState>,
|
||||
Path(profile_user_uuid): Path<Uuid>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Query(params): Query<crate::dtos::ProfileQueryParams>,
|
||||
) -> impl IntoResponse {
|
||||
// Content negotiation: AP clients request application/activity+json
|
||||
let accept = headers.get(axum::http::header::ACCEPT)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
if accept.contains("application/activity+json") || accept.contains("application/ld+json") {
|
||||
return match state.ap_service.actor_json(&profile_user_uuid.to_string()).await {
|
||||
Ok(json) => (
|
||||
[(axum::http::header::CONTENT_TYPE, "application/activity+json")],
|
||||
json,
|
||||
).into_response(),
|
||||
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut ctx = build_page_context(&state, user_id.clone()).await;
|
||||
let view = params.view.unwrap_or_else(|| "recent".to_string());
|
||||
let view_str = params.view.as_deref().unwrap_or("recent");
|
||||
let profile_view = match application::queries::ProfileView::from_str(view_str) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return (axum::http::StatusCode::BAD_REQUEST, "invalid view parameter").into_response(),
|
||||
};
|
||||
|
||||
let profile_user = match state.app_ctx.user_repository
|
||||
.find_by_id(&domain::value_objects::UserId::from_uuid(profile_user_uuid))
|
||||
@@ -308,8 +330,7 @@ pub mod html {
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let display_name = profile_user.email().value()
|
||||
.split('@').next().unwrap_or("User");
|
||||
let display_name = profile_user.username().value();
|
||||
ctx.page_title = format!("{}'s Diary — Movies Diary", display_name);
|
||||
ctx.canonical_url = format!("{}/users/{}", state.app_ctx.config.base_url, profile_user_uuid);
|
||||
|
||||
@@ -328,9 +349,25 @@ pub mod html {
|
||||
0
|
||||
};
|
||||
|
||||
let pending_followers = if is_own_profile {
|
||||
state.ap_service
|
||||
.get_pending_followers(domain::value_objects::UserId::from_uuid(profile_user_uuid))
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|a| application::ports::RemoteActorView {
|
||||
handle: a.handle,
|
||||
url: a.url,
|
||||
display_name: a.display_name,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let query = application::queries::GetUserProfileQuery {
|
||||
user_id: profile_user_uuid,
|
||||
view: view.clone(),
|
||||
view: profile_view,
|
||||
limit: params.limit,
|
||||
offset: params.offset,
|
||||
};
|
||||
@@ -349,7 +386,7 @@ pub mod html {
|
||||
profile_user_id: profile_user_uuid,
|
||||
profile_user_email: profile_user.email().value().to_string(),
|
||||
stats: profile.stats,
|
||||
view,
|
||||
view: profile_view.as_str().to_string(),
|
||||
entries: profile.entries,
|
||||
current_offset: offset,
|
||||
has_more,
|
||||
@@ -359,6 +396,7 @@ pub mod html {
|
||||
is_own_profile,
|
||||
error: params.error,
|
||||
following_count,
|
||||
pending_followers,
|
||||
};
|
||||
match state.html_renderer.render_profile_page(data) {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
@@ -406,6 +444,42 @@ pub mod html {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn accept_follower(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
Path(profile_user_uuid): Path<Uuid>,
|
||||
Form(form): Form<FollowerActionForm>,
|
||||
) -> impl IntoResponse {
|
||||
if user_id.value() != profile_user_uuid {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
match state.ap_service.accept_follower(user_id, &form.actor_url).await {
|
||||
Ok(_) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(),
|
||||
Err(e) => {
|
||||
let msg = encode_error(&e.to_string());
|
||||
Redirect::to(&format!("/users/{}?error={}", profile_user_uuid, msg)).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reject_follower(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
Path(profile_user_uuid): Path<Uuid>,
|
||||
Form(form): Form<FollowerActionForm>,
|
||||
) -> impl IntoResponse {
|
||||
if user_id.value() != profile_user_uuid {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
match state.ap_service.reject_follower(user_id, &form.actor_url).await {
|
||||
Ok(_) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(),
|
||||
Err(e) => {
|
||||
let msg = encode_error(&e.to_string());
|
||||
Redirect::to(&format!("/users/{}?error={}", profile_user_uuid, msg)).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_following_page(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -459,6 +533,11 @@ pub mod posters {
|
||||
State(state): State<AppState>,
|
||||
Path(path): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
// If path is a remote URL, redirect directly instead of serving from local storage.
|
||||
if path.starts_with("http://") || path.starts_with("https://") {
|
||||
return axum::response::Redirect::temporary(&path).into_response();
|
||||
}
|
||||
|
||||
let poster_path = match PosterPath::new(path) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return StatusCode::BAD_REQUEST.into_response(),
|
||||
@@ -672,6 +751,7 @@ pub mod api {
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
register_uc::execute(&state.app_ctx, RegisterCommand {
|
||||
email: req.email,
|
||||
username: req.username,
|
||||
password: req.password,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -98,6 +98,7 @@ async fn wire_dependencies() -> anyhow::Result<AppState> {
|
||||
ActivityPubService::new(
|
||||
federation_repo,
|
||||
Arc::clone(&user_repository),
|
||||
Arc::clone(&repository),
|
||||
app_config.base_url.clone(),
|
||||
cfg!(debug_assertions),
|
||||
)
|
||||
|
||||
@@ -112,6 +112,14 @@ fn html_routes(rate_limit: u64) -> Router<AppState> {
|
||||
"/users/{id}/unfollow",
|
||||
routing::post(handlers::html::unfollow_remote_user),
|
||||
)
|
||||
.route(
|
||||
"/users/{id}/followers/accept",
|
||||
routing::post(handlers::html::accept_follower),
|
||||
)
|
||||
.route(
|
||||
"/users/{id}/followers/reject",
|
||||
routing::post(handlers::html::reject_follower),
|
||||
)
|
||||
.route(
|
||||
"/users/{id}/following-list",
|
||||
routing::get(handlers::html::get_following_page),
|
||||
@@ -127,7 +135,7 @@ fn html_routes(rate_limit: u64) -> Router<AppState> {
|
||||
routing::post(handlers::html::post_delete_review),
|
||||
)
|
||||
.route(
|
||||
"/posters/{path}",
|
||||
"/posters/{*path}",
|
||||
routing::get(handlers::posters::get_poster),
|
||||
)
|
||||
.route("/feed.rss", routing::get(handlers::rss::get_feed))
|
||||
|
||||
@@ -83,6 +83,7 @@ struct NobodyUserRepo;
|
||||
#[async_trait]
|
||||
impl UserRepository for NobodyUserRepo {
|
||||
async fn find_by_email(&self, _: &Email) -> Result<Option<User>, DomainError> { Ok(None) }
|
||||
async fn find_by_username(&self, _: &domain::value_objects::Username) -> Result<Option<User>, DomainError> { Ok(None) }
|
||||
async fn save(&self, _: &User) -> Result<(), DomainError> { panic!() }
|
||||
async fn find_by_id(&self, _: &UserId) -> Result<Option<User>, DomainError> { panic!() }
|
||||
async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, DomainError> { panic!() }
|
||||
@@ -103,12 +104,15 @@ async fn test_ap_service() -> Arc<activitypub::ActivityPubService> {
|
||||
#[async_trait]
|
||||
impl UserRepository for DummyUserRepo {
|
||||
async fn find_by_email(&self, _: &Email) -> Result<Option<User>, DomainError> { Ok(None) }
|
||||
async fn find_by_username(&self, _: &domain::value_objects::Username) -> Result<Option<User>, DomainError> { Ok(None) }
|
||||
async fn save(&self, _: &User) -> Result<(), DomainError> { Ok(()) }
|
||||
async fn find_by_id(&self, _: &UserId) -> Result<Option<User>, DomainError> { Ok(None) }
|
||||
async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, DomainError> { Ok(vec![]) }
|
||||
}
|
||||
let movie_pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
||||
let movie_repo = Arc::new(sqlite::SqliteMovieRepository::new(movie_pool));
|
||||
Arc::new(
|
||||
activitypub::ActivityPubService::new(fed_repo, Arc::new(DummyUserRepo), "http://localhost:3000".to_string(), true)
|
||||
activitypub::ActivityPubService::new(fed_repo, Arc::new(DummyUserRepo), movie_repo, "http://localhost:3000".to_string(), true)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user