Skip to main content

max / makenotwork

7.6 KB · 260 lines History Blame Raw
1 //! Multithreaded forum thread provisioning for published items and blog posts.
2
3 use sqlx::PgPool;
4
5 use crate::Integrations;
6 use crate::background::BackgroundTx;
7 use crate::config::Config;
8 use crate::db;
9 use crate::db::{DbBlogPost, DbItem};
10
11 /// Fire-and-forget: create an MT discussion thread for a published item.
12 /// Called from the item update handler where the user is available.
13 pub fn spawn_mt_thread_for_item(
14 db: &PgPool,
15 bg: &BackgroundTx,
16 integrations: &Integrations,
17 config: &Config,
18 item: &DbItem,
19 user: &crate::auth::SessionUser,
20 ) {
21 let Some(ref mt) = integrations.mt_client else {
22 return;
23 };
24 let mt = mt.clone();
25 let db_pool = db.clone();
26 let host_url = config.host_url.clone();
27 let item_id = item.id;
28 let item_title = item.title.clone();
29 let project_id = item.project_id;
30 let user_id = *user.id;
31 let username = user.username.to_string();
32 let display_name = user.display_name.clone();
33
34 bg.spawn("mt-thread-item", async move {
35 create_mt_thread_for_item(
36 &mt,
37 &db_pool,
38 &host_url,
39 item_id,
40 &item_title,
41 project_id,
42 user_id,
43 &username,
44 display_name.as_deref(),
45 )
46 .await;
47 });
48 }
49
50 /// Fire-and-forget: create an MT discussion thread for a published item
51 /// (scheduler version, looks up the project/user from DB).
52 pub(super) fn spawn_mt_thread_for_item_by_lookup(
53 db: &PgPool,
54 bg: &BackgroundTx,
55 integrations: &Integrations,
56 config: &Config,
57 item: &DbItem,
58 ) {
59 let Some(ref mt) = integrations.mt_client else {
60 return;
61 };
62 let mt = mt.clone();
63 let db_pool = db.clone();
64 let host_url = config.host_url.clone();
65 let item_id = item.id;
66 let item_title = item.title.clone();
67 let project_id = item.project_id;
68
69 bg.spawn("mt-thread-item-lookup", async move {
70 let Ok(Some(project)) = db::projects::get_project_by_id(&db_pool, project_id).await else {
71 return;
72 };
73 let Ok(Some(user)) = db::users::get_user_by_id(&db_pool, project.user_id).await else {
74 return;
75 };
76 create_mt_thread_for_item(
77 &mt,
78 &db_pool,
79 &host_url,
80 item_id,
81 &item_title,
82 project_id,
83 *user.id,
84 &user.username,
85 user.display_name.as_deref(),
86 )
87 .await;
88 });
89 }
90
91 #[allow(clippy::too_many_arguments)]
92 async fn create_mt_thread_for_item(
93 mt: &crate::mt_client::MtClient,
94 db_pool: &sqlx::PgPool,
95 host_url: &str,
96 item_id: db::ItemId,
97 item_title: &str,
98 project_id: db::ProjectId,
99 user_id: uuid::Uuid,
100 username: &str,
101 display_name: Option<&str>,
102 ) {
103 let Ok(Some(project)) = db::projects::get_project_by_id(db_pool, project_id).await else {
104 return;
105 };
106
107 let item_url = format!("{host_url}/i/{item_id}");
108 let body = format!("Discussion for [{item_title}]({item_url})");
109 let external_ref = format!("mnw:item:{item_id}");
110
111 match mt
112 .create_thread(&crate::mt_client::CreateThreadRequest {
113 community_slug: project.slug.to_string(),
114 category_slug: "items".to_string(),
115 title: item_title.to_string(),
116 body_markdown: body,
117 author_mnw_id: user_id,
118 author_username: username.to_string(),
119 author_display_name: display_name.map(String::from),
120 external_ref,
121 })
122 .await
123 {
124 Ok(resp) => {
125 if let Err(e) = db::items::set_mt_thread_id(db_pool, item_id, resp.thread_id).await {
126 tracing::warn!(error = ?e, "failed to store MT thread ID for item");
127 }
128 }
129 Err(e) => tracing::warn!(error = ?e, %item_id, "MT thread creation failed for item"),
130 }
131 }
132
133 /// Fire-and-forget: create an MT discussion thread for a published blog post.
134 /// Called from the blog post handler where the user is available.
135 pub fn spawn_mt_thread_for_blog_post(
136 db: &PgPool,
137 bg: &BackgroundTx,
138 integrations: &Integrations,
139 config: &Config,
140 post: &DbBlogPost,
141 user: &crate::auth::SessionUser,
142 ) {
143 let Some(ref mt) = integrations.mt_client else {
144 return;
145 };
146 let mt = mt.clone();
147 let db_pool = db.clone();
148 let host_url = config.host_url.clone();
149 let post_id = post.id;
150 let post_title = post.title.clone();
151 let post_slug = post.slug.to_string();
152 let project_id = post.project_id;
153 let user_id = *user.id;
154 let username = user.username.to_string();
155 let display_name = user.display_name.clone();
156
157 bg.spawn("mt-thread-blog", async move {
158 create_mt_thread_for_blog_post(
159 &mt,
160 &db_pool,
161 &host_url,
162 post_id,
163 &post_title,
164 &post_slug,
165 project_id,
166 user_id,
167 &username,
168 display_name.as_deref(),
169 )
170 .await;
171 });
172 }
173
174 /// Fire-and-forget: create an MT discussion thread for a published blog post
175 /// (scheduler version, looks up the project/user from DB).
176 pub(super) fn spawn_mt_thread_for_blog_post_by_lookup(
177 db: &PgPool,
178 bg: &BackgroundTx,
179 integrations: &Integrations,
180 config: &Config,
181 post: &DbBlogPost,
182 ) {
183 let Some(ref mt) = integrations.mt_client else {
184 return;
185 };
186 let mt = mt.clone();
187 let db_pool = db.clone();
188 let host_url = config.host_url.clone();
189 let post_id = post.id;
190 let post_title = post.title.clone();
191 let post_slug = post.slug.to_string();
192 let project_id = post.project_id;
193
194 bg.spawn("mt-thread-blog-lookup", async move {
195 let Ok(Some(project)) = db::projects::get_project_by_id(&db_pool, project_id).await else {
196 return;
197 };
198 let Ok(Some(user)) = db::users::get_user_by_id(&db_pool, project.user_id).await else {
199 return;
200 };
201 create_mt_thread_for_blog_post(
202 &mt,
203 &db_pool,
204 &host_url,
205 post_id,
206 &post_title,
207 &post_slug,
208 project_id,
209 *user.id,
210 &user.username,
211 user.display_name.as_deref(),
212 )
213 .await;
214 });
215 }
216
217 #[allow(clippy::too_many_arguments)]
218 async fn create_mt_thread_for_blog_post(
219 mt: &crate::mt_client::MtClient,
220 db_pool: &sqlx::PgPool,
221 host_url: &str,
222 post_id: db::BlogPostId,
223 post_title: &str,
224 post_slug: &str,
225 project_id: db::ProjectId,
226 user_id: uuid::Uuid,
227 username: &str,
228 display_name: Option<&str>,
229 ) {
230 let Ok(Some(project)) = db::projects::get_project_by_id(db_pool, project_id).await else {
231 return;
232 };
233
234 let post_url = format!("{}/{}/blog/{}", host_url, project.slug, post_slug);
235 let body = format!("Discussion for [{post_title}]({post_url})");
236 let external_ref = format!("mnw:blog:{post_id}");
237
238 match mt
239 .create_thread(&crate::mt_client::CreateThreadRequest {
240 community_slug: project.slug.to_string(),
241 category_slug: "blog".to_string(),
242 title: post_title.to_string(),
243 body_markdown: body,
244 author_mnw_id: user_id,
245 author_username: username.to_string(),
246 author_display_name: display_name.map(String::from),
247 external_ref,
248 })
249 .await
250 {
251 Ok(resp) => {
252 if let Err(e) = db::blog_posts::set_mt_thread_id(db_pool, post_id, resp.thread_id).await
253 {
254 tracing::warn!(error = ?e, "failed to store MT thread ID for blog post");
255 }
256 }
257 Err(e) => tracing::warn!(error = ?e, %post_id, "MT thread creation failed for blog post"),
258 }
259 }
260