feat: feature flags
This commit is contained in:
@@ -220,6 +220,7 @@ mod tests {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "federation")]
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::SocialQueryPort for Panic {
|
||||
async fn get_accepted_following_urls(
|
||||
@@ -435,7 +436,9 @@ mod tests {
|
||||
},
|
||||
html_renderer: Arc::new(Panic),
|
||||
rss_renderer: Arc::new(Panic),
|
||||
#[cfg(feature = "federation")]
|
||||
ap_service: Arc::new(activitypub::NoopActivityPubService),
|
||||
#[cfg(feature = "federation")]
|
||||
social_query: Arc::new(Panic),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,27 +15,25 @@ pub mod html {
|
||||
|
||||
use application::{
|
||||
commands::{DeleteReviewCommand, ExportCommand, LoginCommand, RegisterCommand},
|
||||
ports::{
|
||||
FollowersPageData, FollowingPageData, HtmlPageContext, LoginPageData,
|
||||
NewReviewPageData, RegisterPageData, RemoteActorView,
|
||||
},
|
||||
ports::{HtmlPageContext, LoginPageData, NewReviewPageData, RegisterPageData, RemoteActorView},
|
||||
use_cases::{
|
||||
delete_review, export_diary as export_diary_uc, log_review, login as login_uc,
|
||||
register as register_uc,
|
||||
},
|
||||
};
|
||||
#[cfg(feature = "federation")]
|
||||
use application::ports::{FollowersPageData, FollowingPageData};
|
||||
use domain::models::ExportFormat;
|
||||
use domain::{errors::DomainError, value_objects::UserId};
|
||||
|
||||
use crate::{
|
||||
csrf::CsrfToken,
|
||||
dtos::{
|
||||
ErrorQuery, FeedQueryParams, FollowForm, FollowerActionForm, LogReviewData,
|
||||
LogReviewForm, LoginForm, RegisterForm, UnfollowForm,
|
||||
},
|
||||
dtos::{ErrorQuery, FeedQueryParams, LogReviewData, LogReviewForm, LoginForm, RegisterForm},
|
||||
extractors::{OptionalCookieUser, RequiredCookieUser},
|
||||
state::AppState,
|
||||
};
|
||||
#[cfg(feature = "federation")]
|
||||
use crate::dtos::{FollowForm, FollowerActionForm, UnfollowForm};
|
||||
|
||||
async fn build_page_context(
|
||||
state: &AppState,
|
||||
@@ -349,11 +347,15 @@ pub mod html {
|
||||
let limit = params.limit.unwrap_or(20);
|
||||
let offset = params.offset.unwrap_or(0);
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
let filter_str = if params.filter == "following" && user_id.is_some() {
|
||||
"following"
|
||||
} else {
|
||||
"all"
|
||||
};
|
||||
#[cfg(not(feature = "federation"))]
|
||||
let filter_str = "all";
|
||||
|
||||
let sort_by_str = match params.sort_by.as_str() {
|
||||
"date_asc" => "date_asc",
|
||||
"rating" => "rating",
|
||||
@@ -361,6 +363,7 @@ pub mod html {
|
||||
_ => "date",
|
||||
};
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
let following = if filter_str == "following" {
|
||||
if let Some(uid) = user_id {
|
||||
let urls = state.social_query
|
||||
@@ -389,6 +392,8 @@ pub mod html {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
#[cfg(not(feature = "federation"))]
|
||||
let following: Option<domain::ports::FollowingFilter> = None;
|
||||
|
||||
let search_opt = if params.search.is_empty() {
|
||||
None
|
||||
@@ -438,6 +443,7 @@ pub mod html {
|
||||
ctx.page_title = "Members — Movies Diary".to_string();
|
||||
ctx.canonical_url = format!("{}/users", state.app_ctx.config.base_url);
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
let (users_result, actors_result) = tokio::join!(
|
||||
application::use_cases::get_users::execute(
|
||||
&state.app_ctx,
|
||||
@@ -445,6 +451,15 @@ pub mod html {
|
||||
),
|
||||
state.social_query.list_all_followed_remote_actors()
|
||||
);
|
||||
#[cfg(not(feature = "federation"))]
|
||||
let (users_result, actors_result) = (
|
||||
application::use_cases::get_users::execute(
|
||||
&state.app_ctx,
|
||||
application::queries::GetUsersQuery,
|
||||
)
|
||||
.await,
|
||||
Ok::<Vec<domain::ports::RemoteActorInfo>, domain::errors::DomainError>(vec![]),
|
||||
);
|
||||
|
||||
match (users_result, actors_result) {
|
||||
(Ok(users), Ok(remote_actors)) => {
|
||||
@@ -480,26 +495,29 @@ pub mod html {
|
||||
Extension(csrf): Extension<CsrfToken>,
|
||||
) -> 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(),
|
||||
};
|
||||
#[cfg(feature = "federation")]
|
||||
{
|
||||
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(), csrf.0).await;
|
||||
@@ -545,6 +563,7 @@ pub mod html {
|
||||
.map(|u| u.value() == profile_user_uuid)
|
||||
.unwrap_or(false);
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
let following_count = if is_own_profile {
|
||||
if let Some(ref uid) = user_id {
|
||||
state
|
||||
@@ -558,7 +577,10 @@ pub mod html {
|
||||
} else {
|
||||
0
|
||||
};
|
||||
#[cfg(not(feature = "federation"))]
|
||||
let following_count = 0usize;
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
let followers_count = if is_own_profile {
|
||||
state
|
||||
.ap_service
|
||||
@@ -568,8 +590,11 @@ pub mod html {
|
||||
} else {
|
||||
0
|
||||
};
|
||||
#[cfg(not(feature = "federation"))]
|
||||
let followers_count = 0usize;
|
||||
|
||||
let pending_followers = if is_own_profile {
|
||||
#[cfg(feature = "federation")]
|
||||
let pending_followers: Vec<application::ports::RemoteActorView> = if is_own_profile {
|
||||
state
|
||||
.ap_service
|
||||
.get_pending_followers(profile_user_uuid)
|
||||
@@ -585,6 +610,8 @@ pub mod html {
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
#[cfg(not(feature = "federation"))]
|
||||
let pending_followers: Vec<application::ports::RemoteActorView> = vec![];
|
||||
|
||||
let query = application::queries::GetUserProfileQuery {
|
||||
user_id: profile_user_uuid,
|
||||
@@ -639,6 +666,7 @@ pub mod html {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn follow_remote_user(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -662,6 +690,7 @@ pub mod html {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn unfollow_remote_user(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -693,6 +722,7 @@ pub mod html {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn accept_follower(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -719,6 +749,7 @@ pub mod html {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn reject_follower(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -745,6 +776,7 @@ pub mod html {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn get_following_page(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -793,6 +825,7 @@ pub mod html {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn get_followers_page(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -845,6 +878,7 @@ pub mod html {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn remove_follower(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -1010,17 +1044,19 @@ pub mod api {
|
||||
|
||||
use crate::{
|
||||
dtos::{
|
||||
ActivityFeedQueryParams, ActivityFeedResponse, ActorListResponse, ActorUrlRequest,
|
||||
ActivityFeedQueryParams, ActivityFeedResponse,
|
||||
DiaryEntryDto, DiaryQueryParams, DiaryResponse, DirectorStatDto, ExportQueryParams,
|
||||
FeedEntryDto, FollowRequest, LogReviewData, LogReviewRequest, LoginRequest,
|
||||
FeedEntryDto, LogReviewData, LogReviewRequest, LoginRequest,
|
||||
LoginResponse, MonthActivityDto, MonthlyRatingDto, MovieDto, RegisterRequest,
|
||||
RemoteActorDto, ReviewDto, ReviewHistoryResponse, UserProfileQueryParams,
|
||||
ReviewDto, ReviewHistoryResponse, UserProfileQueryParams,
|
||||
UserProfileResponse, UserStatsDto, UserSummaryDto, UserTrendsDto, UsersResponse,
|
||||
},
|
||||
errors::ApiError,
|
||||
extractors::AuthenticatedUser,
|
||||
state::AppState,
|
||||
};
|
||||
#[cfg(feature = "federation")]
|
||||
use crate::dtos::{ActorListResponse, ActorUrlRequest, FollowRequest, RemoteActorDto};
|
||||
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/diary",
|
||||
@@ -1246,11 +1282,13 @@ pub mod api {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
fn ap_err(e: anyhow::Error) -> impl IntoResponse {
|
||||
tracing::error!("ActivityPub error: {:?}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/social/following",
|
||||
responses(
|
||||
@@ -1279,6 +1317,7 @@ pub mod api {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/social/followers",
|
||||
responses(
|
||||
@@ -1307,6 +1346,7 @@ pub mod api {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
post, path = "/api/v1/social/follow",
|
||||
request_body = FollowRequest,
|
||||
@@ -1327,6 +1367,7 @@ pub mod api {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
post, path = "/api/v1/social/unfollow",
|
||||
request_body = ActorUrlRequest,
|
||||
@@ -1347,6 +1388,7 @@ pub mod api {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
post, path = "/api/v1/social/followers/accept",
|
||||
request_body = ActorUrlRequest,
|
||||
@@ -1367,6 +1409,7 @@ pub mod api {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
post, path = "/api/v1/social/followers/reject",
|
||||
request_body = ActorUrlRequest,
|
||||
@@ -1387,6 +1430,7 @@ pub mod api {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
post, path = "/api/v1/social/followers/remove",
|
||||
request_body = ActorUrlRequest,
|
||||
@@ -1407,6 +1451,7 @@ pub mod api {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/social/followers/pending",
|
||||
responses(
|
||||
@@ -1549,12 +1594,19 @@ pub mod api {
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
let following_count = state.ap_service.count_following(user_id).await.unwrap_or(0);
|
||||
#[cfg(not(feature = "federation"))]
|
||||
let following_count = 0usize;
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
let followers_count = state
|
||||
.ap_service
|
||||
.count_accepted_followers(user_id)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
#[cfg(not(feature = "federation"))]
|
||||
let followers_count = 0usize;
|
||||
|
||||
let entries = profile.entries.map(|p| DiaryResponse {
|
||||
items: p.items.iter().map(entry_to_dto).collect(),
|
||||
|
||||
@@ -5,16 +5,25 @@ use event_publisher::{EventPublisherConfig, NoopEventPublisher, create_event_cha
|
||||
use presentation::event_handlers::PosterSyncHandler;
|
||||
use std::str::FromStr;
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
use sqlx::sqlite::SqliteConnectOptions;
|
||||
use tokio::net::TcpListener;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use sqlite::{SqliteMovieRepository, SqliteUserRepository};
|
||||
#[cfg(feature = "sqlite-federation")]
|
||||
use sqlite_federation::SqliteFederationRepository;
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
use postgres::{PostgresRepository, PostgresUserRepository};
|
||||
#[cfg(feature = "postgres-federation")]
|
||||
use postgres_federation::PostgresFederationRepository;
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
use activitypub::{
|
||||
ActivityPubEventHandler, ActivityPubPort, ActivityPubService, DomainUserRepoAdapter,
|
||||
ReviewObjectHandler,
|
||||
};
|
||||
use activitypub::FederationRepository;
|
||||
|
||||
use application::{config::AppConfig, context::AppContext};
|
||||
use auth::{Argon2PasswordHasher, AuthConfig, JwtAuthService};
|
||||
use export::ExportAdapter;
|
||||
@@ -22,10 +31,6 @@ use metadata::MetadataClientImpl;
|
||||
use poster_fetcher::{PosterFetcherConfig, ReqwestPosterFetcher};
|
||||
use poster_storage::{PosterStorageAdapter, StorageConfig};
|
||||
use rss::RssAdapter;
|
||||
use sqlite::{SqliteMovieRepository, SqliteUserRepository};
|
||||
use sqlite_federation::SqliteFederationRepository;
|
||||
use postgres::{PostgresRepository, PostgresUserRepository};
|
||||
use postgres_federation::PostgresFederationRepository;
|
||||
use template_askama::AskamaHtmlRenderer;
|
||||
|
||||
use doc::ApiDocExt;
|
||||
@@ -38,6 +43,9 @@ use domain::ports::{
|
||||
UserRepository,
|
||||
};
|
||||
|
||||
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
|
||||
compile_error!("At least one database backend must be enabled. Use --features sqlite or --features postgres");
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
@@ -77,14 +85,35 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
||||
let auth_service: Arc<dyn AuthService> = Arc::new(JwtAuthService::new(auth_config));
|
||||
let password_hasher: Arc<dyn PasswordHasher> = Arc::new(Argon2PasswordHasher);
|
||||
|
||||
let (movie_repository, review_repository, diary_repository, stats_repository,
|
||||
user_repository, federation_repo_dyn, review_store, social_query) =
|
||||
if backend == "postgres" {
|
||||
wire_postgres(&database_url).await?
|
||||
} else {
|
||||
wire_sqlite(&database_url).await?
|
||||
// Only track pools when the federation feature for that backend needs them
|
||||
#[cfg(feature = "sqlite-federation")]
|
||||
let mut sqlite_pool: Option<sqlx::SqlitePool> = None;
|
||||
#[cfg(feature = "postgres-federation")]
|
||||
let mut pg_pool: Option<sqlx::PgPool> = None;
|
||||
|
||||
let (movie_repository, review_repository, diary_repository, stats_repository, user_repository):
|
||||
(Arc<dyn MovieRepository>, Arc<dyn ReviewRepository>, Arc<dyn DiaryRepository>,
|
||||
Arc<dyn StatsRepository>, Arc<dyn UserRepository>) =
|
||||
match backend.as_str() {
|
||||
#[cfg(feature = "postgres")]
|
||||
"postgres" => {
|
||||
let (_pool, m, r, d, s, u) = wire_postgres(&database_url).await?;
|
||||
#[cfg(feature = "postgres-federation")]
|
||||
{ pg_pool = Some(_pool); }
|
||||
(m, r, d, s, u)
|
||||
}
|
||||
#[cfg(feature = "sqlite")]
|
||||
_ => {
|
||||
let (_pool, m, r, d, s, u) = wire_sqlite(&database_url).await?;
|
||||
#[cfg(feature = "sqlite-federation")]
|
||||
{ sqlite_pool = Some(_pool); }
|
||||
(m, r, d, s, u)
|
||||
}
|
||||
#[cfg(not(feature = "sqlite"))]
|
||||
_ => anyhow::bail!("DATABASE_BACKEND={backend} is not supported by this build (sqlite feature is not enabled)"),
|
||||
};
|
||||
|
||||
// Build handler context (used for poster sync handler)
|
||||
let handler_ctx = AppContext {
|
||||
movie_repository: Arc::clone(&movie_repository),
|
||||
review_repository: Arc::clone(&review_repository),
|
||||
@@ -101,38 +130,77 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
||||
config: app_config.clone(),
|
||||
};
|
||||
|
||||
let user_repo_adapter = Arc::new(DomainUserRepoAdapter(Arc::clone(&user_repository)));
|
||||
let review_handler = Arc::new(ReviewObjectHandler {
|
||||
movie_repository: Arc::clone(&movie_repository),
|
||||
diary_repository: Arc::clone(&diary_repository),
|
||||
review_store,
|
||||
base_url: app_config.base_url.clone(),
|
||||
});
|
||||
let concrete_ap_service = Arc::new(
|
||||
ActivityPubService::new(
|
||||
federation_repo_dyn,
|
||||
user_repo_adapter,
|
||||
review_handler,
|
||||
app_config.base_url.clone(),
|
||||
cfg!(debug_assertions),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
let ap_router = concrete_ap_service.router();
|
||||
let ap_event_handler = ActivityPubEventHandler::new(
|
||||
Arc::clone(&concrete_ap_service),
|
||||
Arc::clone(&movie_repository),
|
||||
Arc::clone(&review_repository),
|
||||
app_config.base_url.clone(),
|
||||
);
|
||||
let ap_service: Arc<dyn ActivityPubPort> = concrete_ap_service;
|
||||
// Wire up event channel, federation service, and ap_router
|
||||
#[cfg(feature = "federation")]
|
||||
let (event_publisher_arc, ap_router, ap_service, social_query) = {
|
||||
let (federation_repo, social_query_arc, review_store): (
|
||||
Arc<dyn activitypub::FederationRepository>,
|
||||
Arc<dyn domain::ports::SocialQueryPort>,
|
||||
Arc<dyn activitypub::RemoteReviewRepository>,
|
||||
) = match backend.as_str() {
|
||||
#[cfg(feature = "postgres-federation")]
|
||||
"postgres" => {
|
||||
let pool = pg_pool.as_ref().unwrap().clone();
|
||||
let fed = Arc::new(PostgresFederationRepository::new(pool));
|
||||
(Arc::clone(&fed) as _, Arc::clone(&fed) as _, fed as _)
|
||||
}
|
||||
#[cfg(feature = "sqlite-federation")]
|
||||
_ => {
|
||||
let pool = sqlite_pool.as_ref().unwrap().clone();
|
||||
let fed = Arc::new(SqliteFederationRepository::new(pool));
|
||||
(Arc::clone(&fed) as _, Arc::clone(&fed) as _, fed as _)
|
||||
}
|
||||
#[cfg(not(feature = "sqlite-federation"))]
|
||||
_ => anyhow::bail!("DATABASE_BACKEND={backend} federation is not supported by this build"),
|
||||
};
|
||||
|
||||
let poster_handler = PosterSyncHandler::new(handler_ctx, 3);
|
||||
let (event_publisher, event_worker) = create_event_channel(
|
||||
EventPublisherConfig::from_env(),
|
||||
vec![Box::new(poster_handler), Box::new(ap_event_handler)],
|
||||
);
|
||||
tokio::spawn(event_worker.run());
|
||||
let user_repo_adapter = Arc::new(DomainUserRepoAdapter(Arc::clone(&user_repository)));
|
||||
let review_handler = Arc::new(ReviewObjectHandler {
|
||||
movie_repository: Arc::clone(&movie_repository),
|
||||
diary_repository: Arc::clone(&diary_repository),
|
||||
review_store,
|
||||
base_url: app_config.base_url.clone(),
|
||||
});
|
||||
let concrete_ap_service = Arc::new(
|
||||
ActivityPubService::new(
|
||||
federation_repo,
|
||||
user_repo_adapter,
|
||||
review_handler,
|
||||
app_config.base_url.clone(),
|
||||
cfg!(debug_assertions),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
let ap_router = concrete_ap_service.router();
|
||||
let ap_event_handler = ActivityPubEventHandler::new(
|
||||
Arc::clone(&concrete_ap_service),
|
||||
Arc::clone(&movie_repository),
|
||||
Arc::clone(&review_repository),
|
||||
app_config.base_url.clone(),
|
||||
);
|
||||
let ap_service_arc: Arc<dyn ActivityPubPort> = concrete_ap_service;
|
||||
|
||||
let poster_handler = PosterSyncHandler::new(handler_ctx, 3);
|
||||
let (event_publisher, event_worker) = create_event_channel(
|
||||
EventPublisherConfig::from_env(),
|
||||
vec![Box::new(poster_handler), Box::new(ap_event_handler)],
|
||||
);
|
||||
tokio::spawn(event_worker.run());
|
||||
|
||||
let ep: Arc<dyn domain::ports::EventPublisher> = Arc::new(event_publisher);
|
||||
(ep, ap_router, ap_service_arc, social_query_arc)
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "federation"))]
|
||||
let (event_publisher_arc, ap_router): (Arc<dyn domain::ports::EventPublisher>, axum::Router) = {
|
||||
let poster_handler = PosterSyncHandler::new(handler_ctx, 3);
|
||||
let (event_publisher, event_worker) = create_event_channel(
|
||||
EventPublisherConfig::from_env(),
|
||||
vec![Box::new(poster_handler)],
|
||||
);
|
||||
tokio::spawn(event_worker.run());
|
||||
(Arc::new(event_publisher), axum::Router::new())
|
||||
};
|
||||
|
||||
let app_ctx = AppContext {
|
||||
movie_repository,
|
||||
@@ -143,7 +211,7 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
||||
metadata_client,
|
||||
poster_fetcher,
|
||||
poster_storage,
|
||||
event_publisher: Arc::new(event_publisher),
|
||||
event_publisher: event_publisher_arc,
|
||||
auth_service,
|
||||
password_hasher,
|
||||
user_repository,
|
||||
@@ -156,30 +224,31 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
||||
rss_renderer: Arc::new(RssAdapter::new(
|
||||
std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:3000".into()),
|
||||
)),
|
||||
#[cfg(feature = "federation")]
|
||||
ap_service,
|
||||
#[cfg(feature = "federation")]
|
||||
social_query,
|
||||
};
|
||||
Ok((state, ap_router))
|
||||
}
|
||||
|
||||
type WireResult = anyhow::Result<(
|
||||
#[cfg(feature = "sqlite")]
|
||||
async fn wire_sqlite(database_url: &str) -> anyhow::Result<(
|
||||
sqlx::SqlitePool,
|
||||
Arc<dyn MovieRepository>,
|
||||
Arc<dyn ReviewRepository>,
|
||||
Arc<dyn DiaryRepository>,
|
||||
Arc<dyn StatsRepository>,
|
||||
Arc<dyn UserRepository>,
|
||||
Arc<dyn FederationRepository>,
|
||||
Arc<dyn activitypub::RemoteReviewRepository>,
|
||||
Arc<dyn domain::ports::SocialQueryPort>,
|
||||
)>;
|
||||
)> {
|
||||
use sqlx::sqlite::SqliteConnectOptions;
|
||||
|
||||
async fn wire_sqlite(database_url: &str) -> WireResult {
|
||||
let opts = SqliteConnectOptions::from_str(database_url)
|
||||
.context("Invalid DATABASE_URL")?
|
||||
.create_if_missing(true)
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.busy_timeout(std::time::Duration::from_secs(5));
|
||||
let pool = SqlitePool::connect_with(opts)
|
||||
let pool = sqlx::SqlitePool::connect_with(opts)
|
||||
.await
|
||||
.context("Failed to connect to SQLite database")?;
|
||||
|
||||
@@ -197,16 +266,18 @@ async fn wire_sqlite(database_url: &str) -> WireResult {
|
||||
let user_repository: Arc<dyn UserRepository> =
|
||||
Arc::new(SqliteUserRepository::new(pool.clone()));
|
||||
|
||||
let fed = Arc::new(SqliteFederationRepository::new(pool));
|
||||
let federation_repo_dyn: Arc<dyn FederationRepository> = Arc::clone(&fed) as _;
|
||||
let review_store: Arc<dyn activitypub::RemoteReviewRepository> = Arc::clone(&fed) as _;
|
||||
let social_query: Arc<dyn domain::ports::SocialQueryPort> = fed;
|
||||
|
||||
Ok((movie_repository, review_repository, diary_repository, stats_repository,
|
||||
user_repository, federation_repo_dyn, review_store, social_query))
|
||||
Ok((pool, movie_repository, review_repository, diary_repository, stats_repository, user_repository))
|
||||
}
|
||||
|
||||
async fn wire_postgres(database_url: &str) -> WireResult {
|
||||
#[cfg(feature = "postgres")]
|
||||
async fn wire_postgres(database_url: &str) -> anyhow::Result<(
|
||||
sqlx::PgPool,
|
||||
Arc<dyn MovieRepository>,
|
||||
Arc<dyn ReviewRepository>,
|
||||
Arc<dyn DiaryRepository>,
|
||||
Arc<dyn StatsRepository>,
|
||||
Arc<dyn UserRepository>,
|
||||
)> {
|
||||
let pool = sqlx::PgPool::connect(database_url)
|
||||
.await
|
||||
.context("Failed to connect to PostgreSQL database")?;
|
||||
@@ -225,13 +296,7 @@ async fn wire_postgres(database_url: &str) -> WireResult {
|
||||
let user_repository: Arc<dyn UserRepository> =
|
||||
Arc::new(PostgresUserRepository::new(pool.clone()));
|
||||
|
||||
let fed = Arc::new(PostgresFederationRepository::new(pool));
|
||||
let federation_repo_dyn: Arc<dyn FederationRepository> = Arc::clone(&fed) as _;
|
||||
let review_store: Arc<dyn activitypub::RemoteReviewRepository> = Arc::clone(&fed) as _;
|
||||
let social_query: Arc<dyn domain::ports::SocialQueryPort> = fed;
|
||||
|
||||
Ok((movie_repository, review_repository, diary_repository, stats_repository,
|
||||
user_repository, federation_repo_dyn, review_store, social_query))
|
||||
Ok((pool, movie_repository, review_repository, diary_repository, stats_repository, user_repository))
|
||||
}
|
||||
|
||||
fn init_tracing() {
|
||||
|
||||
@@ -4,12 +4,14 @@ use utoipa::{
|
||||
};
|
||||
|
||||
use crate::dtos::{
|
||||
ActivityFeedResponse, ActorListResponse, ActorUrlRequest, DiaryEntryDto, DiaryResponse,
|
||||
DirectorStatDto, FeedEntryDto, FollowRequest, LoginRequest, LoginResponse, LogReviewRequest,
|
||||
MonthActivityDto, MonthlyRatingDto, MovieDto, RegisterRequest, RemoteActorDto, ReviewDto,
|
||||
ActivityFeedResponse, DiaryEntryDto, DiaryResponse,
|
||||
DirectorStatDto, FeedEntryDto, LoginRequest, LoginResponse, LogReviewRequest,
|
||||
MonthActivityDto, MonthlyRatingDto, MovieDto, RegisterRequest, ReviewDto,
|
||||
ReviewHistoryResponse, UserProfileResponse, UserStatsDto, UserSummaryDto, UserTrendsDto,
|
||||
UsersResponse,
|
||||
};
|
||||
#[cfg(feature = "federation")]
|
||||
use crate::dtos::{ActorListResponse, ActorUrlRequest, FollowRequest, RemoteActorDto};
|
||||
|
||||
struct SecurityAddon;
|
||||
|
||||
@@ -23,6 +25,53 @@ impl Modify for SecurityAddon {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "federation"))]
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
info(
|
||||
title = "Movies Diary API",
|
||||
version = "1.0.0",
|
||||
description = "REST API for Movies Diary. Authenticate with `POST /api/v1/auth/login` to get a Bearer token."
|
||||
),
|
||||
paths(
|
||||
crate::handlers::api::get_diary,
|
||||
crate::handlers::api::get_review_history,
|
||||
crate::handlers::api::post_review,
|
||||
crate::handlers::api::delete_review,
|
||||
crate::handlers::api::sync_poster,
|
||||
crate::handlers::api::login,
|
||||
crate::handlers::api::register,
|
||||
crate::handlers::api::export_diary,
|
||||
crate::handlers::api::get_activity_feed,
|
||||
crate::handlers::api::list_users,
|
||||
crate::handlers::api::get_user_profile,
|
||||
),
|
||||
components(schemas(
|
||||
DiaryResponse,
|
||||
DiaryEntryDto,
|
||||
MovieDto,
|
||||
ReviewDto,
|
||||
LogReviewRequest,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RegisterRequest,
|
||||
ReviewHistoryResponse,
|
||||
ActivityFeedResponse,
|
||||
FeedEntryDto,
|
||||
UsersResponse,
|
||||
UserSummaryDto,
|
||||
UserProfileResponse,
|
||||
UserStatsDto,
|
||||
MonthActivityDto,
|
||||
MonthlyRatingDto,
|
||||
DirectorStatDto,
|
||||
UserTrendsDto,
|
||||
)),
|
||||
modifiers(&SecurityAddon),
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
info(
|
||||
|
||||
@@ -43,13 +43,44 @@ fn html_routes(rate_limit: u64) -> Router<AppState> {
|
||||
GovernorLayer::new(cfg)
|
||||
});
|
||||
|
||||
Router::new()
|
||||
let base = Router::new()
|
||||
.route("/", routing::get(handlers::html::get_activity_feed))
|
||||
.route("/users", routing::get(handlers::html::get_users_list))
|
||||
.route(
|
||||
"/users/{id}",
|
||||
routing::get(handlers::html::get_user_profile),
|
||||
)
|
||||
.merge(auth)
|
||||
.route(
|
||||
"/reviews/new",
|
||||
routing::get(handlers::html::get_new_review_page),
|
||||
)
|
||||
.route("/reviews", routing::post(handlers::html::post_review))
|
||||
.route(
|
||||
"/reviews/{id}/delete",
|
||||
routing::post(handlers::html::post_delete_review),
|
||||
)
|
||||
.route(
|
||||
"/posters/{*path}",
|
||||
routing::get(handlers::posters::get_poster),
|
||||
)
|
||||
.route("/diary/export", routing::get(handlers::html::get_export))
|
||||
.route("/feed.rss", routing::get(handlers::rss::get_feed))
|
||||
.route(
|
||||
"/users/{id}/feed.rss",
|
||||
routing::get(handlers::rss::get_user_feed),
|
||||
)
|
||||
.layer(axum::middleware::from_fn(crate::csrf::csrf_middleware));
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
let base = base.merge(federation_html_routes());
|
||||
|
||||
base
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
fn federation_html_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/users/{id}/follow",
|
||||
routing::post(handlers::html::follow_remote_user),
|
||||
@@ -78,27 +109,6 @@ fn html_routes(rate_limit: u64) -> Router<AppState> {
|
||||
"/users/{id}/followers/remove",
|
||||
routing::post(handlers::html::remove_follower),
|
||||
)
|
||||
.merge(auth)
|
||||
.route(
|
||||
"/reviews/new",
|
||||
routing::get(handlers::html::get_new_review_page),
|
||||
)
|
||||
.route("/reviews", routing::post(handlers::html::post_review))
|
||||
.route(
|
||||
"/reviews/{id}/delete",
|
||||
routing::post(handlers::html::post_delete_review),
|
||||
)
|
||||
.route(
|
||||
"/posters/{*path}",
|
||||
routing::get(handlers::posters::get_poster),
|
||||
)
|
||||
.route("/diary/export", routing::get(handlers::html::get_export))
|
||||
.route("/feed.rss", routing::get(handlers::rss::get_feed))
|
||||
.route(
|
||||
"/users/{id}/feed.rss",
|
||||
routing::get(handlers::rss::get_user_feed),
|
||||
)
|
||||
.layer(axum::middleware::from_fn(crate::csrf::csrf_middleware))
|
||||
}
|
||||
|
||||
fn api_routes(rate_limit: u64) -> Router<AppState> {
|
||||
@@ -109,58 +119,64 @@ fn api_routes(rate_limit: u64) -> Router<AppState> {
|
||||
.finish()
|
||||
.unwrap();
|
||||
|
||||
Router::new().nest(
|
||||
"/api/v1",
|
||||
Router::new()
|
||||
.route("/diary", routing::get(handlers::api::get_diary))
|
||||
.route(
|
||||
"/movies/{id}/history",
|
||||
routing::get(handlers::api::get_review_history),
|
||||
)
|
||||
.route("/reviews", routing::post(handlers::api::post_review))
|
||||
.route(
|
||||
"/reviews/{id}",
|
||||
routing::delete(handlers::api::delete_review),
|
||||
)
|
||||
.route(
|
||||
"/movies/{id}/sync-poster",
|
||||
routing::post(handlers::api::sync_poster),
|
||||
)
|
||||
.route("/auth/login", routing::post(handlers::api::login))
|
||||
.route("/auth/register", routing::post(handlers::api::register))
|
||||
.route("/diary/export", routing::get(handlers::api::export_diary))
|
||||
.route(
|
||||
"/activity-feed",
|
||||
routing::get(handlers::api::get_activity_feed),
|
||||
)
|
||||
.route("/users", routing::get(handlers::api::list_users))
|
||||
.route("/users/{id}", routing::get(handlers::api::get_user_profile))
|
||||
.route(
|
||||
"/social/following",
|
||||
routing::get(handlers::api::get_following),
|
||||
)
|
||||
.route(
|
||||
"/social/followers",
|
||||
routing::get(handlers::api::get_followers),
|
||||
)
|
||||
.route(
|
||||
"/social/followers/pending",
|
||||
routing::get(handlers::api::get_pending_followers),
|
||||
)
|
||||
.route("/social/follow", routing::post(handlers::api::follow))
|
||||
.route("/social/unfollow", routing::post(handlers::api::unfollow))
|
||||
.route(
|
||||
"/social/followers/accept",
|
||||
routing::post(handlers::api::accept_follower),
|
||||
)
|
||||
.route(
|
||||
"/social/followers/reject",
|
||||
routing::post(handlers::api::reject_follower),
|
||||
)
|
||||
.route(
|
||||
"/social/followers/remove",
|
||||
routing::post(handlers::api::remove_follower),
|
||||
)
|
||||
.layer(GovernorLayer::new(cfg)),
|
||||
)
|
||||
let base = Router::new()
|
||||
.route("/diary", routing::get(handlers::api::get_diary))
|
||||
.route(
|
||||
"/movies/{id}/history",
|
||||
routing::get(handlers::api::get_review_history),
|
||||
)
|
||||
.route("/reviews", routing::post(handlers::api::post_review))
|
||||
.route(
|
||||
"/reviews/{id}",
|
||||
routing::delete(handlers::api::delete_review),
|
||||
)
|
||||
.route(
|
||||
"/movies/{id}/sync-poster",
|
||||
routing::post(handlers::api::sync_poster),
|
||||
)
|
||||
.route("/auth/login", routing::post(handlers::api::login))
|
||||
.route("/auth/register", routing::post(handlers::api::register))
|
||||
.route("/diary/export", routing::get(handlers::api::export_diary))
|
||||
.route(
|
||||
"/activity-feed",
|
||||
routing::get(handlers::api::get_activity_feed),
|
||||
)
|
||||
.route("/users", routing::get(handlers::api::list_users))
|
||||
.route("/users/{id}", routing::get(handlers::api::get_user_profile));
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
let base = base.merge(federation_api_routes());
|
||||
|
||||
Router::new().nest("/api/v1", base.layer(GovernorLayer::new(cfg)))
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
fn federation_api_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/social/following",
|
||||
routing::get(handlers::api::get_following),
|
||||
)
|
||||
.route(
|
||||
"/social/followers",
|
||||
routing::get(handlers::api::get_followers),
|
||||
)
|
||||
.route(
|
||||
"/social/followers/pending",
|
||||
routing::get(handlers::api::get_pending_followers),
|
||||
)
|
||||
.route("/social/follow", routing::post(handlers::api::follow))
|
||||
.route("/social/unfollow", routing::post(handlers::api::unfollow))
|
||||
.route(
|
||||
"/social/followers/accept",
|
||||
routing::post(handlers::api::accept_follower),
|
||||
)
|
||||
.route(
|
||||
"/social/followers/reject",
|
||||
routing::post(handlers::api::reject_follower),
|
||||
)
|
||||
.route(
|
||||
"/social/followers/remove",
|
||||
routing::post(handlers::api::remove_follower),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use activitypub::ActivityPubPort;
|
||||
use application::context::AppContext;
|
||||
|
||||
use crate::ports::{HtmlRenderer, RssFeedRenderer};
|
||||
@@ -10,6 +9,8 @@ pub struct AppState {
|
||||
pub app_ctx: AppContext,
|
||||
pub html_renderer: Arc<dyn HtmlRenderer>,
|
||||
pub rss_renderer: Arc<dyn RssFeedRenderer>,
|
||||
pub ap_service: Arc<dyn ActivityPubPort>,
|
||||
#[cfg(feature = "federation")]
|
||||
pub ap_service: Arc<dyn activitypub::ActivityPubPort>,
|
||||
#[cfg(feature = "federation")]
|
||||
pub social_query: Arc<dyn domain::ports::SocialQueryPort>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user