i18n+filtering fork - fluent-templates v2
at main 384 lines 15 kB view raw
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(&current_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: &current_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 &current_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: &current_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 &current_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: &current_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(&current_handle), 381 &canonical_url, 382 ) 383 .into_response()) 384}