//! Multithreaded forum thread provisioning for published items and blog posts. use crate::db; use crate::db::{DbBlogPost, DbItem}; use crate::AppState; /// Fire-and-forget: create an MT discussion thread for a published item. /// Called from the item update handler where the user is available. pub fn spawn_mt_thread_for_item( state: &AppState, item: &DbItem, user: &crate::auth::SessionUser, ) { let Some(ref mt) = state.mt_client else { return; }; let mt = mt.clone(); let db_pool = state.db.clone(); let host_url = state.config.host_url.clone(); let item_id = item.id; let item_title = item.title.clone(); let project_id = item.project_id; let user_id = *user.id; let username = user.username.to_string(); let display_name = user.display_name.clone(); tokio::spawn(async move { create_mt_thread_for_item( &mt, &db_pool, &host_url, item_id, &item_title, project_id, user_id, &username, display_name.as_deref(), ) .await; }); } /// Fire-and-forget: create an MT discussion thread for a published item /// (scheduler version — looks up the project/user from DB). pub(super) fn spawn_mt_thread_for_item_by_lookup(state: &AppState, item: &DbItem) { let Some(ref mt) = state.mt_client else { return; }; let mt = mt.clone(); let db_pool = state.db.clone(); let host_url = state.config.host_url.clone(); let item_id = item.id; let item_title = item.title.clone(); let project_id = item.project_id; tokio::spawn(async move { let Ok(Some(project)) = db::projects::get_project_by_id(&db_pool, project_id).await else { return; }; let Ok(Some(user)) = db::users::get_user_by_id(&db_pool, project.user_id).await else { return; }; create_mt_thread_for_item( &mt, &db_pool, &host_url, item_id, &item_title, project_id, *user.id, &user.username, user.display_name.as_deref(), ) .await; }); } #[allow(clippy::too_many_arguments)] async fn create_mt_thread_for_item( mt: &crate::mt_client::MtClient, db_pool: &sqlx::PgPool, host_url: &str, item_id: db::ItemId, item_title: &str, project_id: db::ProjectId, user_id: uuid::Uuid, username: &str, display_name: Option<&str>, ) { let Ok(Some(project)) = db::projects::get_project_by_id(db_pool, project_id).await else { return; }; let item_url = format!("{}/i/{}", host_url, item_id); let body = format!("Discussion for [{}]({})", item_title, item_url); let external_ref = format!("mnw:item:{}", item_id); match mt .create_thread(&crate::mt_client::CreateThreadRequest { community_slug: project.slug.to_string(), category_slug: "items".to_string(), title: item_title.to_string(), body_markdown: body, author_mnw_id: user_id, author_username: username.to_string(), author_display_name: display_name.map(String::from), external_ref, }) .await { Ok(resp) => { if let Err(e) = db::items::set_mt_thread_id(db_pool, item_id, resp.thread_id).await { tracing::warn!(error = ?e, "failed to store MT thread ID for item"); } } Err(e) => tracing::warn!(error = ?e, %item_id, "MT thread creation failed for item"), } } /// Fire-and-forget: create an MT discussion thread for a published blog post. /// Called from the blog post handler where the user is available. pub fn spawn_mt_thread_for_blog_post( state: &AppState, post: &DbBlogPost, user: &crate::auth::SessionUser, ) { let Some(ref mt) = state.mt_client else { return; }; let mt = mt.clone(); let db_pool = state.db.clone(); let host_url = state.config.host_url.clone(); let post_id = post.id; let post_title = post.title.clone(); let post_slug = post.slug.to_string(); let project_id = post.project_id; let user_id = *user.id; let username = user.username.to_string(); let display_name = user.display_name.clone(); tokio::spawn(async move { create_mt_thread_for_blog_post( &mt, &db_pool, &host_url, post_id, &post_title, &post_slug, project_id, user_id, &username, display_name.as_deref(), ) .await; }); } /// Fire-and-forget: create an MT discussion thread for a published blog post /// (scheduler version — looks up the project/user from DB). pub(super) fn spawn_mt_thread_for_blog_post_by_lookup(state: &AppState, post: &DbBlogPost) { let Some(ref mt) = state.mt_client else { return; }; let mt = mt.clone(); let db_pool = state.db.clone(); let host_url = state.config.host_url.clone(); let post_id = post.id; let post_title = post.title.clone(); let post_slug = post.slug.to_string(); let project_id = post.project_id; tokio::spawn(async move { let Ok(Some(project)) = db::projects::get_project_by_id(&db_pool, project_id).await else { return; }; let Ok(Some(user)) = db::users::get_user_by_id(&db_pool, project.user_id).await else { return; }; create_mt_thread_for_blog_post( &mt, &db_pool, &host_url, post_id, &post_title, &post_slug, project_id, *user.id, &user.username, user.display_name.as_deref(), ) .await; }); } #[allow(clippy::too_many_arguments)] async fn create_mt_thread_for_blog_post( mt: &crate::mt_client::MtClient, db_pool: &sqlx::PgPool, host_url: &str, post_id: db::BlogPostId, post_title: &str, post_slug: &str, project_id: db::ProjectId, user_id: uuid::Uuid, username: &str, display_name: Option<&str>, ) { let Ok(Some(project)) = db::projects::get_project_by_id(db_pool, project_id).await else { return; }; let post_url = format!( "{}/{}/blog/{}", host_url, project.slug, post_slug ); let body = format!("Discussion for [{}]({})", post_title, post_url); let external_ref = format!("mnw:blog:{}", post_id); match mt .create_thread(&crate::mt_client::CreateThreadRequest { community_slug: project.slug.to_string(), category_slug: "blog".to_string(), title: post_title.to_string(), body_markdown: body, author_mnw_id: user_id, author_username: username.to_string(), author_display_name: display_name.map(String::from), external_ref, }) .await { Ok(resp) => { if let Err(e) = db::blog_posts::set_mt_thread_id(db_pool, post_id, resp.thread_id).await { tracing::warn!(error = ?e, "failed to store MT thread ID for blog post"); } } Err(e) => tracing::warn!(error = ?e, %post_id, "MT thread creation failed for blog post"), } }