use async_trait::async_trait; use loco_rs::{ Result, app::{AppContext, Initializer}, }; use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel, QueryOrder, Set}; use std::path::{Path, PathBuf}; use crate::models::_entities::{comments, posts, site_settings}; const FIXTURES_DIR: &str = "src/fixtures"; pub struct ContentSyncInitializer; #[async_trait] impl Initializer for ContentSyncInitializer { fn name(&self) -> String { "content-sync".to_string() } async fn before_run(&self, app_context: &AppContext) -> Result<()> { sync_content(app_context, Path::new(FIXTURES_DIR)).await } } async fn sync_content(ctx: &AppContext, base: &Path) -> Result<()> { sync_site_settings(ctx, base).await?; sync_comment_post_slugs(ctx, base).await?; Ok(()) } fn read_fixture_rows(base: &Path, file_name: &str) -> Vec { let path: PathBuf = base.join(file_name); let seed_data = match std::fs::read_to_string(path) { Ok(data) => data, Err(_) => return vec![], }; serde_yaml::from_str(&seed_data).unwrap_or_default() } fn as_optional_string(value: &serde_json::Value) -> Option { value.as_str().and_then(|item| { let trimmed = item.trim(); if trimmed.is_empty() { None } else { Some(trimmed.to_string()) } }) } fn is_blank(value: &Option) -> bool { value.as_deref().map(str::trim).unwrap_or("").is_empty() } fn matches_legacy_ai_defaults(settings: &site_settings::Model) -> bool { let provider = settings.ai_provider.as_deref().map(str::trim); let api_base = settings.ai_api_base.as_deref().map(str::trim); let chat_model = settings.ai_chat_model.as_deref().map(str::trim); (provider == Some("openai-compatible") && api_base == Some("https://api.openai.com/v1") && chat_model == Some("gpt-4.1-mini") && is_blank(&settings.ai_api_key)) || (provider == Some("newapi") && matches!( api_base, Some("https://cliproxy.ai.init.cool") | Some("https://cliproxy.ai.init.cool/v1") ) && chat_model == Some("gpt-5.4")) } async fn sync_site_settings(ctx: &AppContext, base: &Path) -> Result<()> { let rows = read_fixture_rows(base, "site_settings.yaml"); let Some(seed) = rows.first() else { return Ok(()); }; let tech_stack = seed["tech_stack"] .as_array() .map(|items| { items .iter() .filter_map(|item| item.as_str()) .map(ToString::to_string) .collect::>() }) .filter(|items| !items.is_empty()) .map(|items| serde_json::json!(items)); let music_playlist = seed["music_playlist"] .as_array() .map(|items| { items .iter() .filter_map(|item| { let title = item["title"].as_str()?.trim(); let url = item["url"].as_str()?.trim(); if title.is_empty() || url.is_empty() { None } else { Some(serde_json::json!({ "title": title, "url": url, })) } }) .collect::>() }) .filter(|items| !items.is_empty()) .map(serde_json::Value::Array); let music_enabled = seed["music_enabled"].as_bool().or(Some(true)); let maintenance_mode_enabled = seed["maintenance_mode_enabled"].as_bool().or(Some(false)); let maintenance_access_code = as_optional_string(&seed["maintenance_access_code"]); let comment_verification_mode = as_optional_string(&seed["comment_verification_mode"]); let subscription_verification_mode = as_optional_string(&seed["subscription_verification_mode"]); let comment_turnstile_enabled = seed["comment_turnstile_enabled"] .as_bool() .or(comment_verification_mode .as_deref() .map(|value| value.eq_ignore_ascii_case("turnstile"))); let subscription_turnstile_enabled = seed["subscription_turnstile_enabled"] .as_bool() .or(subscription_verification_mode .as_deref() .map(|value| value.eq_ignore_ascii_case("turnstile"))); let existing = site_settings::Entity::find() .order_by_asc(site_settings::Column::Id) .one(&ctx.db) .await?; if let Some(existing) = existing { let mut model = existing.clone().into_active_model(); let should_upgrade_legacy_ai_defaults = matches_legacy_ai_defaults(&existing); if is_blank(&existing.site_name) { model.site_name = Set(as_optional_string(&seed["site_name"])); } if is_blank(&existing.site_short_name) { model.site_short_name = Set(as_optional_string(&seed["site_short_name"])); } if is_blank(&existing.site_url) { model.site_url = Set(as_optional_string(&seed["site_url"])); } if is_blank(&existing.site_title) { model.site_title = Set(as_optional_string(&seed["site_title"])); } if is_blank(&existing.site_description) { model.site_description = Set(as_optional_string(&seed["site_description"])); } if is_blank(&existing.hero_title) { model.hero_title = Set(as_optional_string(&seed["hero_title"])); } if is_blank(&existing.hero_subtitle) { model.hero_subtitle = Set(as_optional_string(&seed["hero_subtitle"])); } if is_blank(&existing.owner_name) { model.owner_name = Set(as_optional_string(&seed["owner_name"])); } if is_blank(&existing.owner_title) { model.owner_title = Set(as_optional_string(&seed["owner_title"])); } if is_blank(&existing.owner_bio) { model.owner_bio = Set(as_optional_string(&seed["owner_bio"])); } if is_blank(&existing.owner_avatar_url) { model.owner_avatar_url = Set(as_optional_string(&seed["owner_avatar_url"])); } if is_blank(&existing.social_github) { model.social_github = Set(as_optional_string(&seed["social_github"])); } if is_blank(&existing.social_twitter) { model.social_twitter = Set(as_optional_string(&seed["social_twitter"])); } if is_blank(&existing.social_email) { model.social_email = Set(as_optional_string(&seed["social_email"])); } if is_blank(&existing.location) { model.location = Set(as_optional_string(&seed["location"])); } if existing.tech_stack.is_none() { model.tech_stack = Set(tech_stack); } if existing.music_playlist.is_none() { model.music_playlist = Set(music_playlist); } if existing.music_enabled.is_none() { model.music_enabled = Set(music_enabled); } if existing.maintenance_mode_enabled.is_none() { model.maintenance_mode_enabled = Set(maintenance_mode_enabled); } if is_blank(&existing.maintenance_access_code) { model.maintenance_access_code = Set(maintenance_access_code.clone()); } if existing.ai_enabled.is_none() { model.ai_enabled = Set(seed["ai_enabled"].as_bool()); } if existing.paragraph_comments_enabled.is_none() { model.paragraph_comments_enabled = Set(seed["paragraph_comments_enabled"].as_bool().or(Some(true))); } if existing.comment_verification_mode.is_none() { model.comment_verification_mode = Set(comment_verification_mode.clone()); } if existing.comment_turnstile_enabled.is_none() { model.comment_turnstile_enabled = Set(comment_turnstile_enabled); } if existing.subscription_verification_mode.is_none() { model.subscription_verification_mode = Set(subscription_verification_mode.clone()); } if existing.subscription_turnstile_enabled.is_none() { model.subscription_turnstile_enabled = Set(subscription_turnstile_enabled); } if is_blank(&existing.turnstile_site_key) { model.turnstile_site_key = Set(as_optional_string(&seed["turnstile_site_key"])); } if is_blank(&existing.turnstile_secret_key) { model.turnstile_secret_key = Set(as_optional_string(&seed["turnstile_secret_key"])); } if should_upgrade_legacy_ai_defaults { model.ai_provider = Set(as_optional_string(&seed["ai_provider"])); model.ai_api_base = Set(as_optional_string(&seed["ai_api_base"])); model.ai_api_key = Set(as_optional_string(&seed["ai_api_key"])); model.ai_chat_model = Set(as_optional_string(&seed["ai_chat_model"])); } if is_blank(&existing.ai_provider) { model.ai_provider = Set(as_optional_string(&seed["ai_provider"])); } if is_blank(&existing.ai_api_base) { model.ai_api_base = Set(as_optional_string(&seed["ai_api_base"])); } if is_blank(&existing.ai_api_key) { model.ai_api_key = Set(as_optional_string(&seed["ai_api_key"])); } if is_blank(&existing.ai_chat_model) { model.ai_chat_model = Set(as_optional_string(&seed["ai_chat_model"])); } if is_blank(&existing.ai_embedding_model) { model.ai_embedding_model = Set(as_optional_string(&seed["ai_embedding_model"])); } if is_blank(&existing.ai_system_prompt) { model.ai_system_prompt = Set(as_optional_string(&seed["ai_system_prompt"])); } if existing.ai_top_k.is_none() { model.ai_top_k = Set(seed["ai_top_k"].as_i64().map(|value| value as i32)); } if existing.ai_chunk_size.is_none() { model.ai_chunk_size = Set(seed["ai_chunk_size"].as_i64().map(|value| value as i32)); } let _ = model.update(&ctx.db).await; return Ok(()); } let model = site_settings::ActiveModel { id: Set(seed["id"].as_i64().unwrap_or(1) as i32), site_name: Set(as_optional_string(&seed["site_name"])), site_short_name: Set(as_optional_string(&seed["site_short_name"])), site_url: Set(as_optional_string(&seed["site_url"])), site_title: Set(as_optional_string(&seed["site_title"])), site_description: Set(as_optional_string(&seed["site_description"])), hero_title: Set(as_optional_string(&seed["hero_title"])), hero_subtitle: Set(as_optional_string(&seed["hero_subtitle"])), owner_name: Set(as_optional_string(&seed["owner_name"])), owner_title: Set(as_optional_string(&seed["owner_title"])), owner_bio: Set(as_optional_string(&seed["owner_bio"])), owner_avatar_url: Set(as_optional_string(&seed["owner_avatar_url"])), social_github: Set(as_optional_string(&seed["social_github"])), social_twitter: Set(as_optional_string(&seed["social_twitter"])), social_email: Set(as_optional_string(&seed["social_email"])), location: Set(as_optional_string(&seed["location"])), tech_stack: Set(tech_stack), music_playlist: Set(music_playlist), music_enabled: Set(music_enabled), maintenance_mode_enabled: Set(maintenance_mode_enabled), maintenance_access_code: Set(maintenance_access_code), ai_enabled: Set(seed["ai_enabled"].as_bool()), paragraph_comments_enabled: Set(seed["paragraph_comments_enabled"] .as_bool() .or(Some(true))), comment_verification_mode: Set(comment_verification_mode), comment_turnstile_enabled: Set(comment_turnstile_enabled), subscription_verification_mode: Set(subscription_verification_mode), subscription_turnstile_enabled: Set(subscription_turnstile_enabled), turnstile_site_key: Set(as_optional_string(&seed["turnstile_site_key"])), turnstile_secret_key: Set(as_optional_string(&seed["turnstile_secret_key"])), ai_provider: Set(as_optional_string(&seed["ai_provider"])), ai_api_base: Set(as_optional_string(&seed["ai_api_base"])), ai_api_key: Set(as_optional_string(&seed["ai_api_key"])), ai_chat_model: Set(as_optional_string(&seed["ai_chat_model"])), ai_embedding_model: Set(as_optional_string(&seed["ai_embedding_model"])), ai_system_prompt: Set(as_optional_string(&seed["ai_system_prompt"])), ai_top_k: Set(seed["ai_top_k"].as_i64().map(|value| value as i32)), ai_chunk_size: Set(seed["ai_chunk_size"].as_i64().map(|value| value as i32)), ..Default::default() }; let _ = model.insert(&ctx.db).await; Ok(()) } async fn sync_comment_post_slugs(ctx: &AppContext, base: &Path) -> Result<()> { let rows = read_fixture_rows(base, "comments.yaml"); for seed in rows { let id = seed["id"].as_i64().unwrap_or(0) as i32; let pid = seed["pid"].as_i64().unwrap_or(0) as i32; if id == 0 || pid == 0 { continue; } let Some(existing) = comments::Entity::find_by_id(id).one(&ctx.db).await? else { continue; }; if existing.post_slug.is_some() { continue; } let Some(post) = posts::Entity::find_by_id(pid).one(&ctx.db).await? else { continue; }; let mut model = existing.into_active_model(); model.post_slug = Set(Some(post.slug)); let _ = model.update(&ctx.db).await; } Ok(()) }