i18n+filtering fork - fluent-templates v2
at main 328 lines 12 kB view raw
1use anyhow::Result; 2use axum::{ 3 extract::{Path, State}, 4 response::IntoResponse, 5}; 6use axum_extra::extract::Cached; 7use axum_htmx::{HxBoosted, HxRequest}; 8use minijinja::context as template_context; 9use std::collections::HashMap; 10 11use crate::{ 12 atproto::{ 13 auth::SimpleOAuthSessionProvider, 14 client::{OAuthPdsClient, PutRecordRequest}, 15 lexicon::{ 16 community::lexicon::calendar::event::{ 17 Event as CommunityEvent, EventLink, EventLocation as CommunityLocation, Mode, 18 Status, NSID as COMMUNITY_NSID, 19 }, 20 community::lexicon::location, 21 events::smokesignal::calendar::event::{ 22 Event as SmokeSignalEvent, Location as SmokeSignalLocation, PlaceLocation, 23 NSID as SMOKESIGNAL_NSID, 24 }, 25 }, 26 }, 27 create_renderer, 28 http::{ 29 context::WebContext, 30 errors::{MigrateEventError, WebError}, 31 middleware_auth::Auth, 32 middleware_i18n::Language, 33 utils::url_from_aturi, 34 }, 35 resolve::{parse_input, InputType}, 36 storage::{ 37 event::{event_get, event_insert_with_metadata}, 38 handle::{handle_for_did, handle_for_handle, model::Handle}, 39 }, 40}; 41 42pub async fn handle_migrate_event( 43 State(web_context): State<WebContext>, 44 HxBoosted(hx_boosted): HxBoosted, 45 Language(language): Language, 46 Cached(auth): Cached<Auth>, 47 HxRequest(hx_request): HxRequest, 48 Path((handle_slug, event_rkey)): Path<(String, String)>, 49) -> Result<impl IntoResponse, WebError> { 50 let current_handle = auth.require(&web_context.config.destination_key, "/")?; 51 52 // Create the template renderer with enhanced context 53 let renderer = create_renderer!(web_context.clone(), Language(language), hx_boosted, hx_request); 54 let canonical_url = format!("https://{}/{}/{}/migrate", renderer.web_context.config.external_base, handle_slug, event_rkey); 55 56 // Lookup the user handle/profile 57 let profile: Result<Handle> = match parse_input(&handle_slug) { 58 Ok(InputType::Handle(handle)) => handle_for_handle(&web_context.pool, &handle) 59 .await 60 .map_err(|err| err.into()), 61 Ok(InputType::Plc(did) | InputType::Web(did)) => handle_for_did(&web_context.pool, &did) 62 .await 63 .map_err(|err| err.into()), 64 Err(err) => Err(err.into()), 65 }; 66 67 if let Err(err) = profile { 68 return Ok(renderer.render_error( 69 err, 70 template_context! { 71 current_handle => current_handle.handle.as_str(), 72 canonical_url => canonical_url, 73 }, 74 )); 75 } 76 let profile = profile.unwrap(); 77 78 // Construct AT URI for the source event 79 let source_aturi = format!("at://{}/{}/{}", profile.did, SMOKESIGNAL_NSID, event_rkey); 80 81 // Check if the user is authorized to migrate this event (must be the event creator/organizer) 82 if profile.did != current_handle.did { 83 return Ok(renderer.render_error( 84 MigrateEventError::NotAuthorized, 85 template_context! { 86 current_handle => current_handle.handle.as_str(), 87 canonical_url => canonical_url, 88 }, 89 )); 90 } 91 92 // Fetch the event from the database 93 let event = event_get(&web_context.pool, &source_aturi).await; 94 if let Err(err) = event { 95 return Ok(renderer.render_error( 96 err, 97 template_context! { 98 current_handle => current_handle.handle.as_str(), 99 canonical_url => canonical_url, 100 }, 101 )); 102 } 103 let event = event.unwrap(); 104 105 // Check that this is a smokesignal event (we only migrate those) 106 if event.lexicon != SMOKESIGNAL_NSID { 107 // If it's already the community event type, we don't need to migrate 108 if event.lexicon == COMMUNITY_NSID { 109 return Ok(renderer.render_error( 110 MigrateEventError::AlreadyMigrated, 111 template_context! { 112 current_handle => current_handle.handle.as_str(), 113 canonical_url => canonical_url, 114 }, 115 )); 116 } 117 118 return Ok(renderer.render_error( 119 MigrateEventError::UnsupportedEventType, 120 template_context! { 121 current_handle => current_handle.handle.as_str(), 122 canonical_url => canonical_url, 123 }, 124 )); 125 } 126 127 // Parse the legacy event 128 let legacy_event = match serde_json::from_value::<SmokeSignalEvent>(event.record.0.clone()) { 129 Ok(event) => event, 130 Err(err) => { 131 return Ok(renderer.render_error( 132 err, 133 template_context! { 134 current_handle => current_handle.handle.as_str(), 135 canonical_url => canonical_url, 136 }, 137 )); 138 } 139 }; 140 141 // Extract data from the legacy event 142 let (name, text, created_at, starts_at, extra) = match legacy_event { 143 SmokeSignalEvent::Current { 144 name, 145 text, 146 created_at, 147 starts_at, 148 extra, 149 } => (name, text, created_at, starts_at, extra), 150 }; 151 152 // Extract optional fields from the extra map 153 let ends_at = extra 154 .get("endsAt") 155 .and_then(|v| v.as_str()) 156 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) 157 .map(|dt| dt.with_timezone(&chrono::Utc)); 158 159 // Extract legacy mode/status 160 let legacy_mode = extra.get("mode").and_then(|v| v.as_str()); 161 let legacy_status = extra.get("status").and_then(|v| v.as_str()); 162 163 // Convert mode to the community format 164 let mode = match legacy_mode { 165 Some("events.smokesignal.calendar.event#inperson") => Some(Mode::InPerson), 166 Some("events.smokesignal.calendar.event#virtual") => Some(Mode::Virtual), 167 _ => None, 168 }; 169 170 // Convert status to the community format 171 let status = match legacy_status { 172 Some("events.smokesignal.calendar.event#scheduled") => Some(Status::Scheduled), 173 Some("events.smokesignal.calendar.event#cancelled") => Some(Status::Cancelled), 174 Some("events.smokesignal.calendar.event#postponed") => Some(Status::Postponed), 175 Some("events.smokesignal.calendar.event#rescheduled") => Some(Status::Rescheduled), 176 _ => Some(Status::Scheduled), // Default to scheduled if not specified 177 }; 178 179 // Helper function to convert PlaceLocation to community Address 180 fn convert_place_to_address(place: &PlaceLocation) -> location::Address { 181 location::Address::Current { 182 country: place.country.clone().unwrap_or_default(), 183 postal_code: place.postal_code.clone(), 184 region: place.region.clone(), 185 locality: place.locality.clone(), 186 street: place.street.clone(), 187 name: Some(place.name.clone()), 188 } 189 } 190 191 // Extract locations and links from the legacy event 192 let mut locations = Vec::new(); 193 let mut uris = Vec::new(); 194 195 if let Some(location_values) = extra.get("location") { 196 if let Some(location_array) = location_values.as_array() { 197 for location_value in location_array { 198 // Parse the location 199 if let Ok(location) = 200 serde_json::from_value::<SmokeSignalLocation>(location_value.clone()) 201 { 202 match location { 203 SmokeSignalLocation::Place(place) => { 204 // Convert place location to community address 205 let address = convert_place_to_address(&place); 206 locations.push(CommunityLocation::Address(address)); 207 } 208 SmokeSignalLocation::Virtual(virtual_loc) => { 209 // Convert virtual locations to EventLink elements 210 if let Some(url) = &virtual_loc.url { 211 uris.push(EventLink::Current { 212 uri: url.clone(), 213 name: Some(virtual_loc.name.clone()), 214 }); 215 } 216 } 217 } 218 } 219 } 220 } 221 } 222 223 // Create a new community event 224 let new_event = CommunityEvent::Current { 225 name: name.clone(), 226 description: text.unwrap_or_default(), 227 created_at: created_at.unwrap_or_else(chrono::Utc::now), 228 starts_at, 229 ends_at, 230 mode, 231 status, 232 locations, 233 uris, 234 extra: HashMap::default(), 235 }; 236 237 // Construct the target AT-URI for the new community event 238 let migrated_aturi = format!("at://{}/{}/{}", profile.did, COMMUNITY_NSID, event_rkey); 239 240 // Check if a record already exists at the target AT-URI 241 let existing_event = event_get(&web_context.pool, &migrated_aturi).await; 242 if existing_event.is_ok() { 243 return Ok(renderer.render_error( 244 MigrateEventError::DestinationExists, 245 template_context! { 246 current_handle => current_handle.handle.as_str(), 247 canonical_url => canonical_url, 248 }, 249 )); 250 } 251 252 // Set up XRPC client 253 // Error if we don't have auth data 254 let auth_data = auth.1.ok_or(MigrateEventError::NotAuthorized)?; 255 let client_auth: SimpleOAuthSessionProvider = SimpleOAuthSessionProvider::try_from(auth_data)?; 256 257 let client = OAuthPdsClient { 258 http_client: &web_context.http_client, 259 pds: &current_handle.pds, 260 }; 261 262 // Create the community event record in the user's PDS using putRecord to retain the same rkey 263 let update_record_request = PutRecordRequest { 264 repo: current_handle.did.clone(), 265 collection: COMMUNITY_NSID.to_string(), 266 record_key: event_rkey.clone(), 267 record: new_event.clone(), 268 validate: false, 269 swap_commit: None, 270 swap_record: None, // We're creating a new record, not replacing 271 }; 272 273 // Write to the PDS 274 let update_record_result = client.put_record(&client_auth, update_record_request).await; 275 if let Err(err) = update_record_result { 276 return Ok(renderer.render_error( 277 err, 278 template_context! { 279 current_handle => current_handle.handle.as_str(), 280 canonical_url => canonical_url, 281 }, 282 )); 283 } 284 // update_record_result is guaranteed to be Ok at this point since we checked for Err above 285 let update_record_result = update_record_result.unwrap(); 286 287 // We already have the migrated AT-URI defined above 288 289 // Insert the migrated event into the database 290 let migrated_event_insert_result = event_insert_with_metadata( 291 &web_context.pool, 292 &migrated_aturi, 293 &update_record_result.cid, 294 &current_handle.did, 295 COMMUNITY_NSID, 296 &new_event, 297 &name, 298 ) 299 .await; 300 301 if let Err(err) = migrated_event_insert_result { 302 return Ok(renderer.render_error( 303 err, 304 template_context! { 305 current_handle => current_handle.handle.as_str(), 306 canonical_url => canonical_url, 307 }, 308 )); 309 } 310 311 // Generate URL for the migrated event 312 let migrated_event_url = url_from_aturi(&web_context.config.external_base, &migrated_aturi)?; 313 314 // Return success with migration complete template 315 Ok(renderer.render_template( 316 "migrate_event", 317 template_context! { 318 migrated_event_url => migrated_event_url, 319 source_aturi => source_aturi, 320 migrated_aturi => migrated_aturi, 321 event_name => name, 322 source_lexicon => SMOKESIGNAL_NSID, 323 target_lexicon => COMMUNITY_NSID, 324 }, 325 Some(&current_handle), 326 &canonical_url, 327 )) 328}