WIP: federation + integrations
Some checks failed
CI / Check / Test (push) Failing after 5m56s

This commit is contained in:
2026-06-02 19:50:19 +02:00
parent dcc9244d4e
commit ac7edd6953
19 changed files with 660 additions and 1352 deletions

View File

@@ -13,34 +13,44 @@ use domain::ports::{RemoteWatchlistRepository, SocialQueryPort};
use crate::config::AppConfig;
#[derive(Clone)]
pub struct AppContext {
pub movie_repository: Arc<dyn MovieRepository>,
pub review_repository: Arc<dyn ReviewRepository>,
pub diary_repository: Arc<dyn DiaryRepository>,
pub diary_exporter: Arc<dyn DiaryExporter>,
pub document_parser: Arc<dyn DocumentParser>,
pub stats_repository: Arc<dyn StatsRepository>,
pub metadata_client: Arc<dyn MetadataClient>,
pub poster_fetcher: Arc<dyn PosterFetcherClient>,
pub image_storage: Arc<dyn ImageStorage>,
pub event_publisher: Arc<dyn EventPublisher>,
pub auth_service: Arc<dyn AuthService>,
pub password_hasher: Arc<dyn PasswordHasher>,
pub user_repository: Arc<dyn UserRepository>,
pub import_session_repository: Arc<dyn ImportSessionRepository>,
pub import_profile_repository: Arc<dyn ImportProfileRepository>,
pub movie_profile_repository: Arc<dyn MovieProfileRepository>,
pub struct Repositories {
pub movie: Arc<dyn MovieRepository>,
pub review: Arc<dyn ReviewRepository>,
pub diary: Arc<dyn DiaryRepository>,
pub stats: Arc<dyn StatsRepository>,
pub user: Arc<dyn UserRepository>,
pub import_session: Arc<dyn ImportSessionRepository>,
pub import_profile: Arc<dyn ImportProfileRepository>,
pub movie_profile: Arc<dyn MovieProfileRepository>,
pub watchlist: Arc<dyn WatchlistRepository>,
pub watch_event: Arc<dyn WatchEventRepository>,
pub webhook_token: Arc<dyn WebhookTokenRepository>,
pub person_command: Arc<dyn PersonCommand>,
pub person_query: Arc<dyn PersonQuery>,
pub search_port: Arc<dyn SearchPort>,
pub search_command: Arc<dyn SearchCommand>,
pub watchlist_repository: Arc<dyn WatchlistRepository>,
pub watch_event_repository: Arc<dyn WatchEventRepository>,
pub webhook_token_repository: Arc<dyn WebhookTokenRepository>,
pub profile_fields_repository: Arc<dyn UserProfileFieldsRepository>,
pub profile_fields: Arc<dyn UserProfileFieldsRepository>,
#[cfg(feature = "federation")]
pub remote_watchlist_repository: Arc<dyn RemoteWatchlistRepository>,
pub remote_watchlist: Arc<dyn RemoteWatchlistRepository>,
#[cfg(feature = "federation")]
pub social_query: Arc<dyn SocialQueryPort>,
}
#[derive(Clone)]
pub struct Services {
pub auth: Arc<dyn AuthService>,
pub password_hasher: Arc<dyn PasswordHasher>,
pub metadata: Arc<dyn MetadataClient>,
pub poster_fetcher: Arc<dyn PosterFetcherClient>,
pub image_storage: Arc<dyn ImageStorage>,
pub event_publisher: Arc<dyn EventPublisher>,
pub diary_exporter: Arc<dyn DiaryExporter>,
pub document_parser: Arc<dyn DocumentParser>,
}
#[derive(Clone)]
pub struct AppContext {
pub repos: Repositories,
pub services: Services,
pub config: AppConfig,
}

View File

@@ -1,16 +1,6 @@
use uuid::Uuid;
use domain::models::{
DiaryEntry, FeedEntry, MonthActivity, Movie, MovieProfile, MovieStats, UserStats, UserSummary,
UserTrends, collections::Paginated,
};
pub struct RemoteActorView {
pub handle: String,
pub display_name: Option<String>,
pub url: String,
pub avatar_url: Option<String>,
}
use domain::models::DiaryEntry;
pub struct HtmlPageContext {
pub user_email: Option<String>,
@@ -30,239 +20,16 @@ impl HtmlPageContext {
}
}
pub struct LoginPageData<'a> {
pub ctx: HtmlPageContext,
pub error: Option<&'a str>,
}
pub struct RegisterPageData<'a> {
pub ctx: HtmlPageContext,
pub error: Option<&'a str>,
}
pub struct NewReviewPageData<'a> {
pub ctx: HtmlPageContext,
pub error: Option<&'a str>,
}
pub struct ActivityFeedPageData {
pub ctx: HtmlPageContext,
pub entries: Paginated<FeedEntry>,
pub current_offset: u32,
pub has_more: bool,
pub limit: u32,
pub filter: String,
pub sort_by: String,
pub search: String,
}
pub struct UsersPageData {
pub ctx: HtmlPageContext,
pub users: Vec<UserSummary>,
pub remote_actors: Vec<RemoteActorView>,
}
pub struct ProfilePageData {
pub ctx: HtmlPageContext,
pub profile_user_id: Uuid,
pub profile_user_email: String,
pub stats: UserStats,
pub view: String,
pub entries: Option<Paginated<DiaryEntry>>,
pub current_offset: u32,
pub has_more: bool,
pub limit: u32,
pub history: Option<Vec<MonthActivity>>,
pub trends: Option<UserTrends>,
pub is_own_profile: bool,
pub error: Option<String>,
pub following_count: usize,
pub followers_count: usize,
pub pending_followers: Vec<RemoteActorView>,
pub sort_by: String,
pub search: String,
}
pub struct FollowingPageData {
pub ctx: HtmlPageContext,
pub user_id: Uuid,
pub actors: Vec<RemoteActorView>,
pub error: Option<String>,
}
pub struct FollowersPageData {
pub ctx: HtmlPageContext,
pub user_id: Uuid,
pub actors: Vec<RemoteActorView>,
pub error: Option<String>,
}
pub struct MovieDetailPageData {
pub ctx: HtmlPageContext,
pub movie: Movie,
pub stats: MovieStats,
pub reviews: Paginated<FeedEntry>,
pub profile: Option<MovieProfile>,
pub on_watchlist: bool,
pub current_offset: u32,
pub has_more: bool,
pub limit: u32,
pub histogram_max: u64,
}
#[derive(Clone, Debug)]
pub struct WatchlistDisplayEntry {
/// Always a full URL: /images/{path} for local, https://... for remote
pub poster_url: Option<String>,
pub movie_title: String,
pub release_year: u16,
/// /movies/{id} for local; None for remote entries without a local movie record
pub movie_url: Option<String>,
pub added_at: String,
/// /watchlist/{movie_id}/remove for owner; None for remote or non-owner
pub remove_url: Option<String>,
}
pub struct WatchlistPageData {
pub ctx: HtmlPageContext,
pub owner_id: uuid::Uuid,
pub display_entries: Vec<WatchlistDisplayEntry>,
pub current_offset: u32,
pub has_more: bool,
pub limit: u32,
pub is_owner: bool,
pub error: Option<String>,
}
pub struct ImportUploadPageData {
pub ctx: HtmlPageContext,
pub profiles: Vec<ImportProfileView>,
pub error: Option<String>,
}
pub struct ImportProfileView {
pub id: String,
pub name: String,
}
pub struct ImportMappingPageData {
pub ctx: HtmlPageContext,
pub session_id: String,
pub columns: Vec<String>,
pub sample_rows: Vec<Vec<String>>,
pub domain_fields: Vec<(&'static str, &'static str)>,
pub error: Option<String>,
}
pub struct ImportPreviewRow {
pub index: usize,
pub status: ImportRowStatus,
pub cells: Vec<String>,
}
pub enum ImportRowStatus {
Valid,
Duplicate,
Invalid(String),
}
pub struct ImportPreviewPageData {
pub ctx: HtmlPageContext,
pub session_id: String,
pub columns: Vec<String>,
pub rows: Vec<ImportPreviewRow>,
}
pub struct ProfileSettingsPageData {
pub ctx: HtmlPageContext,
pub bio: Option<String>,
pub avatar_url: Option<String>,
pub banner_url: Option<String>,
pub also_known_as: Option<String>,
pub profile_fields: Vec<(String, String)>,
pub saved: bool,
}
pub struct BlockedDomainEntry {
pub domain: String,
pub reason: Option<String>,
pub blocked_at: String,
}
pub struct BlockedDomainsPageData {
pub ctx: HtmlPageContext,
pub domains: Vec<BlockedDomainEntry>,
}
pub struct BlockedActorEntry {
pub url: String,
pub handle: String,
pub display_name: Option<String>,
pub avatar_url: Option<String>,
}
pub struct BlockedActorsPageData {
pub ctx: HtmlPageContext,
pub actors: Vec<BlockedActorEntry>,
}
pub struct WebhookTokenView {
pub id: String,
pub provider: String,
pub label: Option<String>,
pub created_at: String,
pub last_used_at: Option<String>,
}
pub struct IntegrationsPageData {
pub ctx: HtmlPageContext,
pub tokens: Vec<WebhookTokenView>,
pub webhook_base_url: String,
pub new_token: Option<String>,
}
pub struct WatchQueueDisplayEntry {
pub id: String,
pub title: String,
pub year: Option<u16>,
pub source: String,
pub watched_at: String,
pub movie_url: Option<String>,
}
pub struct WatchQueuePageData {
pub ctx: HtmlPageContext,
pub entries: Vec<WatchQueueDisplayEntry>,
pub error: Option<String>,
}
pub trait HtmlRenderer: Send + Sync {
fn render_diary_page(
&self,
data: &Paginated<DiaryEntry>,
ctx: HtmlPageContext,
) -> Result<String, String>;
fn render_login_page(&self, data: LoginPageData<'_>) -> Result<String, String>;
fn render_register_page(&self, data: RegisterPageData<'_>) -> Result<String, String>;
fn render_new_review_page(&self, data: NewReviewPageData<'_>) -> Result<String, String>;
fn render_activity_feed_page(&self, data: ActivityFeedPageData) -> Result<String, String>;
fn render_users_page(&self, data: UsersPageData) -> Result<String, String>;
fn render_profile_page(&self, data: ProfilePageData) -> Result<String, String>;
fn render_following_page(&self, data: FollowingPageData) -> Result<String, String>;
fn render_followers_page(&self, data: FollowersPageData) -> Result<String, String>;
fn render_movie_detail_page(&self, data: MovieDetailPageData) -> Result<String, String>;
fn render_import_upload_page(&self, data: ImportUploadPageData) -> Result<String, String>;
fn render_import_mapping_page(&self, data: ImportMappingPageData) -> Result<String, String>;
fn render_import_preview_page(&self, data: ImportPreviewPageData) -> Result<String, String>;
fn render_profile_settings_page(&self, data: ProfileSettingsPageData)
-> Result<String, String>;
fn render_blocked_domains_page(&self, data: BlockedDomainsPageData) -> Result<String, String>;
fn render_blocked_actors_page(&self, data: BlockedActorsPageData) -> Result<String, String>;
fn render_watchlist_page(&self, data: WatchlistPageData) -> Result<String, String>;
fn render_integrations_page(&self, data: IntegrationsPageData) -> Result<String, String>;
fn render_watch_queue_page(&self, data: WatchQueuePageData) -> Result<String, String>;
}
pub trait RssFeedRenderer: Send + Sync {
fn render_feed(&self, entries: &[DiaryEntry], title: &str) -> Result<String, String>;
}

View File

@@ -23,7 +23,10 @@ use domain::{
},
};
use crate::{config::AppConfig, context::AppContext};
use crate::{
config::AppConfig,
context::{AppContext, Repositories, Services},
};
pub struct TestContextBuilder {
pub movie_repo: Arc<dyn MovieRepository>,
@@ -125,35 +128,39 @@ impl TestContextBuilder {
pub fn build(self) -> AppContext {
AppContext {
movie_repository: self.movie_repo,
review_repository: self.review_repo,
diary_repository: self.diary_repo,
diary_exporter: self.diary_exporter,
document_parser: self.document_parser,
stats_repository: self.stats_repo,
metadata_client: self.metadata_client,
poster_fetcher: self.poster_fetcher,
image_storage: self.image_storage,
event_publisher: self.event_publisher,
auth_service: self.auth_service,
password_hasher: self.password_hasher,
user_repository: self.user_repo,
import_session_repository: self.import_session_repo,
import_profile_repository: self.import_profile_repo,
movie_profile_repository: self.movie_profile_repo,
watchlist_repository: self.watchlist_repo,
watch_event_repository: self.watch_event_repo,
webhook_token_repository: self.webhook_token_repo,
profile_fields_repository: self.profile_fields_repo,
person_command: self.person_command,
person_query: self.person_query,
search_port: self.search_port,
search_command: self.search_command,
repos: Repositories {
movie: self.movie_repo,
review: self.review_repo,
diary: self.diary_repo,
stats: self.stats_repo,
user: self.user_repo,
import_session: self.import_session_repo,
import_profile: self.import_profile_repo,
movie_profile: self.movie_profile_repo,
watchlist: self.watchlist_repo,
watch_event: self.watch_event_repo,
webhook_token: self.webhook_token_repo,
profile_fields: self.profile_fields_repo,
person_command: self.person_command,
person_query: self.person_query,
search_port: self.search_port,
search_command: self.search_command,
#[cfg(feature = "federation")]
remote_watchlist: Arc::new(PanicRemoteWatchlistRepository),
#[cfg(feature = "federation")]
social_query: Arc::new(PanicSocialQueryPort),
},
services: Services {
auth: self.auth_service,
password_hasher: self.password_hasher,
metadata: self.metadata_client,
poster_fetcher: self.poster_fetcher,
image_storage: self.image_storage,
event_publisher: self.event_publisher,
diary_exporter: self.diary_exporter,
document_parser: self.document_parser,
},
config: self.config,
#[cfg(feature = "federation")]
remote_watchlist_repository: std::sync::Arc::new(PanicRemoteWatchlistRepository),
#[cfg(feature = "federation")]
social_query: std::sync::Arc::new(PanicSocialQueryPort),
}
}
}