forked from
smokesignal.events/smokesignal
i18n+filtering fork - fluent-templates v2
1use axum::{
2 extract::{Form, State},
3 response::IntoResponse,
4};
5use axum_extra::extract::Cached;
6use axum_htmx::{HxBoosted, HxRequest};
7use http::StatusCode;
8use minijinja::context as template_context;
9use serde::Deserialize;
10
11use crate::{
12 atproto::{
13 auth::SimpleOAuthSessionProvider,
14 client::{ListRecordsParams, OAuthPdsClient},
15 lexicon::{
16 community::lexicon::calendar::{
17 event::{Event as LexiconCommunityEvent, NSID as LEXICON_COMMUNITY_EVENT_NSID},
18 rsvp::{
19 Rsvp as LexiconCommunityRsvp, RsvpStatus as LexiconCommunityRsvpStatus,
20 NSID as LEXICON_COMMUNITY_RSVP_NSID,
21 },
22 },
23 events::smokesignal::calendar::{
24 event::{Event as SmokeSignalEvent, NSID as SMOKESIGNAL_EVENT_NSID},
25 rsvp::{
26 Rsvp as SmokeSignalRsvp, RsvpStatus as SmokeSignalRsvpStatus,
27 NSID as SMOKESIGNAL_RSVP_NSID,
28 },
29 },
30 },
31 },
32 contextual_error, create_renderer,
33 http::{
34 context::WebContext,
35 errors::{ImportError, WebError},
36 middleware_auth::Auth,
37 middleware_i18n::Language,
38 },
39 storage::event::{event_insert_with_metadata, rsvp_insert_with_metadata},
40};
41
42pub async fn handle_import(
43 State(web_context): State<WebContext>,
44 Language(language): Language,
45 Cached(auth): Cached<Auth>,
46 HxRequest(hx_request): HxRequest,
47 HxBoosted(hx_boosted): HxBoosted,
48) -> Result<impl IntoResponse, WebError> {
49 let current_handle = auth.require(&web_context.config.destination_key, "/import")?;
50
51 // Create the template renderer with enhanced context
52 let renderer = create_renderer!(web_context.clone(), Language(language), hx_boosted, hx_request);
53
54 let canonical_url = format!("https://{}/import", web_context.config.external_base);
55
56 Ok(renderer.render_template(
57 "import",
58 template_context! {},
59 Some(¤t_handle),
60 &canonical_url,
61 ))
62}
63
64#[derive(Debug, Deserialize)]
65pub struct ImportForm {
66 pub collection: Option<String>,
67 pub cursor: Option<String>,
68}
69
70pub async fn handle_import_submit(
71 State(web_context): State<WebContext>,
72 Language(language): Language,
73 Cached(auth): Cached<Auth>,
74 HxRequest(hx_request): HxRequest,
75 Form(import_form): Form<ImportForm>,
76) -> Result<impl IntoResponse, WebError> {
77 let current_handle = auth.require_flat()?;
78
79 if !hx_request {
80 return Ok(StatusCode::BAD_REQUEST.into_response());
81 }
82
83 // Create the template renderer for HTMX partial updates
84 let renderer = create_renderer!(web_context.clone(), Language(language), false, true);
85
86 let canonical_url = format!("https://{}/import", web_context.config.external_base);
87
88 let collections = [
89 LEXICON_COMMUNITY_EVENT_NSID,
90 LEXICON_COMMUNITY_RSVP_NSID,
91 SMOKESIGNAL_EVENT_NSID,
92 SMOKESIGNAL_RSVP_NSID,
93 ];
94
95 let collection = import_form.collection.unwrap_or(collections[0].to_string());
96 let cursor = import_form.cursor;
97
98 let client_auth: SimpleOAuthSessionProvider =
99 SimpleOAuthSessionProvider::try_from(auth.1.unwrap())?;
100 let client = OAuthPdsClient {
101 http_client: &web_context.http_client,
102 pds: ¤t_handle.pds,
103 };
104
105 const LIMIT: u32 = 20;
106
107 // Set up list records parameters to fetch records
108 let list_params = ListRecordsParams {
109 repo: current_handle.did.clone(),
110 collection: collection.clone(),
111 limit: Some(LIMIT),
112 cursor,
113 reverse: None,
114 };
115
116 let render_context = match collection.as_str() {
117 LEXICON_COMMUNITY_EVENT_NSID => {
118 let results = client
119 .list_records::<LexiconCommunityEvent>(&client_auth, &list_params)
120 .await;
121 match results {
122 Ok(list_records) => {
123 let mut items = vec![];
124
125 for event_record in list_records.records {
126 let name = match &event_record.value {
127 LexiconCommunityEvent::Current { name, .. } => name.clone(),
128 };
129 let event_insert_resp = event_insert_with_metadata(
130 &web_context.pool,
131 &event_record.uri,
132 &event_record.cid,
133 ¤t_handle.did,
134 LEXICON_COMMUNITY_EVENT_NSID,
135 &event_record.value,
136 &name,
137 )
138 .await;
139
140 let is_ok = if let Err(err) = event_insert_resp {
141 tracing::error!(?err, "error inserting event");
142 false
143 } else {
144 true
145 };
146
147 items.push(format!("{} - {}", event_record.uri, is_ok));
148 }
149
150 let (collection, cursor) = if items.len() == LIMIT as usize {
151 (collection.to_string(), Some(list_records.cursor))
152 } else {
153 (LEXICON_COMMUNITY_RSVP_NSID.to_string(), None)
154 };
155
156 template_context! {
157 cursor,
158 items_paged => true,
159 items,
160 collection,
161 completed => false,
162 }
163 }
164 Err(err) => {
165 return contextual_error!(
166 renderer: renderer,
167 ImportError::FailedToListCommunityEvents(err.to_string()),
168 template_context!{}
169 )
170 }
171 }
172 }
173 LEXICON_COMMUNITY_RSVP_NSID => {
174 let results = client
175 .list_records::<LexiconCommunityRsvp>(&client_auth, &list_params)
176 .await;
177 match results {
178 Ok(list_records) => {
179 let mut items = vec![];
180
181 for rsvp_record in list_records.records {
182 let (event_uri, event_cid, status) = match &rsvp_record.value {
183 LexiconCommunityRsvp::Current {
184 subject, status, ..
185 } => {
186 let status_str = match status {
187 LexiconCommunityRsvpStatus::Going => "going",
188 LexiconCommunityRsvpStatus::Interested => "interested",
189 LexiconCommunityRsvpStatus::NotGoing => "notgoing",
190 };
191 (subject.uri.clone(), subject.cid.clone(), status_str)
192 }
193 };
194
195 let rsvp_insert_resp = rsvp_insert_with_metadata(
196 &web_context.pool,
197 crate::storage::event::RsvpInsertParams {
198 aturi: &rsvp_record.uri,
199 cid: &rsvp_record.cid,
200 did: ¤t_handle.did,
201 lexicon: LEXICON_COMMUNITY_RSVP_NSID,
202 record: &rsvp_record.value,
203 event_aturi: &event_uri,
204 event_cid: &event_cid,
205 status,
206 },
207 )
208 .await;
209
210 let is_ok = if let Err(err) = rsvp_insert_resp {
211 tracing::error!(?err, "error inserting community RSVP");
212 false
213 } else {
214 true
215 };
216
217 items.push(format!("{} - {}", rsvp_record.uri, is_ok));
218 }
219
220 let (collection, cursor) = if items.len() == LIMIT as usize {
221 (collection.to_string(), Some(list_records.cursor))
222 } else {
223 (SMOKESIGNAL_EVENT_NSID.to_string(), None)
224 };
225
226 template_context! {
227 cursor,
228 items_paged => true,
229 items,
230 collection,
231 completed => false,
232 }
233 }
234 Err(err) => {
235 return contextual_error!(
236 renderer: renderer,
237 ImportError::FailedToListCommunityRSVPs(err.to_string()),
238 template_context!{}
239 )
240 }
241 }
242 }
243 SMOKESIGNAL_EVENT_NSID => {
244 let results = client
245 .list_records::<SmokeSignalEvent>(&client_auth, &list_params)
246 .await;
247 match results {
248 Ok(list_records) => {
249 let mut items = vec![];
250
251 for event_record in list_records.records {
252 let name = match &event_record.value {
253 SmokeSignalEvent::Current { name, .. } => name.clone(),
254 };
255 let event_insert_resp = event_insert_with_metadata(
256 &web_context.pool,
257 &event_record.uri,
258 &event_record.cid,
259 ¤t_handle.did,
260 SMOKESIGNAL_EVENT_NSID,
261 &event_record.value,
262 &name,
263 )
264 .await;
265
266 let is_ok = if let Err(err) = event_insert_resp {
267 tracing::error!(?err, "error inserting Smokesignal event");
268 false
269 } else {
270 true
271 };
272
273 items.push(format!("{} - {}", event_record.uri, is_ok));
274 }
275
276 let (collection, cursor) = if items.len() == LIMIT as usize {
277 (collection.to_string(), Some(list_records.cursor))
278 } else {
279 (SMOKESIGNAL_RSVP_NSID.to_string(), None)
280 };
281
282 template_context! {
283 cursor,
284 items_paged => true,
285 items,
286 collection,
287 completed => false,
288 }
289 }
290 Err(err) => {
291 return contextual_error!(
292 renderer: renderer,
293 ImportError::FailedToListSmokesignalEvents(err.to_string()),
294 template_context!{}
295 )
296 }
297 }
298 }
299 SMOKESIGNAL_RSVP_NSID => {
300 let results = client
301 .list_records::<SmokeSignalRsvp>(&client_auth, &list_params)
302 .await;
303 match results {
304 Ok(list_records) => {
305 let mut items = vec![];
306
307 for rsvp_record in list_records.records {
308 let (event_uri, event_cid, status) = match &rsvp_record.value {
309 SmokeSignalRsvp::Current {
310 subject, status, ..
311 } => {
312 let status_str = match status {
313 SmokeSignalRsvpStatus::Going => "going",
314 SmokeSignalRsvpStatus::Interested => "interested",
315 SmokeSignalRsvpStatus::NotGoing => "notgoing",
316 };
317 (subject.uri.clone(), subject.cid.clone(), status_str)
318 }
319 };
320
321 let rsvp_insert_resp = rsvp_insert_with_metadata(
322 &web_context.pool,
323 crate::storage::event::RsvpInsertParams {
324 aturi: &rsvp_record.uri,
325 cid: &rsvp_record.cid,
326 did: ¤t_handle.did,
327 lexicon: SMOKESIGNAL_RSVP_NSID,
328 record: &rsvp_record.value,
329 event_aturi: &event_uri,
330 event_cid: &event_cid,
331 status,
332 },
333 )
334 .await;
335
336 let is_ok = if let Err(err) = rsvp_insert_resp {
337 tracing::error!(?err, "error inserting Smokesignal RSVP");
338 false
339 } else {
340 true
341 };
342
343 items.push(format!("{} - {}", rsvp_record.uri, is_ok));
344 }
345
346 let completed = items.len() == LIMIT as usize;
347
348 template_context! {
349 cursor => list_records.cursor,
350 items_paged => true,
351 items,
352 collection,
353 completed,
354 }
355 }
356 Err(err) => {
357 return contextual_error!(
358 renderer: renderer,
359 ImportError::FailedToListSmokesignalRSVPs(err.to_string()),
360 template_context!{}
361 )
362 }
363 }
364 }
365 _ => {
366 return contextual_error!(
367 renderer: renderer,
368 ImportError::UnsupportedCollectionType(collection.clone()),
369 template_context!{}
370 )
371 }
372 };
373
374 Ok(renderer
375 .render_template(
376 "import",
377 template_context! {
378 ..render_context,
379 },
380 Some(¤t_handle),
381 &canonical_url,
382 )
383 .into_response())
384}