The smokesignal.events web application
56
fork

Configure Feed

Select the types of activity you want to include in your feed.

feature: community.lexicon.calendar.getRSVP

+770 -446
+12 -9
build.rs
··· 5 5 println!("cargo:rerun-if-changed=static/.vite/manifest.json"); 6 6 7 7 // Read Vite manifest to get hashed asset filenames 8 - let manifest_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) 9 - .join("static/.vite/manifest.json"); 8 + let manifest_path = 9 + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("static/.vite/manifest.json"); 10 10 11 11 let (bundle_js, bundle_css) = if manifest_path.exists() { 12 - let manifest_content = std::fs::read_to_string(&manifest_path) 13 - .expect("Failed to read Vite manifest"); 14 - let manifest: serde_json::Value = serde_json::from_str(&manifest_content) 15 - .expect("Failed to parse Vite manifest"); 12 + let manifest_content = 13 + std::fs::read_to_string(&manifest_path).expect("Failed to read Vite manifest"); 14 + let manifest: serde_json::Value = 15 + serde_json::from_str(&manifest_content).expect("Failed to parse Vite manifest"); 16 16 17 17 // Extract main entry point 18 - let main_entry = manifest.get("src/main.ts") 18 + let main_entry = manifest 19 + .get("src/main.ts") 19 20 .expect("Missing src/main.ts in manifest"); 20 21 21 - let js_file = main_entry.get("file") 22 + let js_file = main_entry 23 + .get("file") 22 24 .and_then(|v| v.as_str()) 23 25 .expect("Missing file in main entry"); 24 26 25 - let css_file = main_entry.get("css") 27 + let css_file = main_entry 28 + .get("css") 26 29 .and_then(|v| v.as_array()) 27 30 .and_then(|arr| arr.first()) 28 31 .and_then(|v| v.as_str())
+1 -4
src/atproto/lexicon/lfg.rs
··· 214 214 let mut lfg = create_test_lfg(); 215 215 lfg.tags = vec![]; 216 216 assert!(lfg.validate().is_err()); 217 - assert_eq!( 218 - lfg.validate().unwrap_err(), 219 - "At least one tag is required" 220 - ); 217 + assert_eq!(lfg.validate().unwrap_err(), "At least one tag is required"); 221 218 } 222 219 223 220 #[test]
+3 -1
src/atproto/utils.rs
··· 99 99 #[cfg(test)] 100 100 mod tests { 101 101 use super::*; 102 - use atproto_record::lexicon::app::bsky::richtext::facet::{ByteSlice, Facet, Link, Mention, Tag}; 102 + use atproto_record::lexicon::app::bsky::richtext::facet::{ 103 + ByteSlice, Facet, Link, Mention, Tag, 104 + }; 103 105 use std::collections::HashMap; 104 106 105 107 #[test]
+7 -6
src/bin/smokesignal.rs
··· 5 5 }; 6 6 use atproto_oauth_axum::state::OAuthClientConfig; 7 7 use smokesignal::processor::ContentFetcher; 8 + use smokesignal::service::{ServiceDID, ServiceKey, build_service_document}; 9 + use smokesignal::storage::content::{CachedContentStorage, ContentStorage, FilesystemStorage}; 8 10 use smokesignal::tap_processor::TapProcessor; 9 11 use smokesignal::task_search_indexer::SearchIndexer; 10 - use tokio_util::sync::CancellationToken; 11 - use smokesignal::service::{ServiceDID, ServiceKey, build_service_document}; 12 - use smokesignal::storage::content::{CachedContentStorage, ContentStorage, FilesystemStorage}; 13 12 use smokesignal::{ 14 13 http::{ 15 14 context::{AppEngine, WebContext}, ··· 22 21 cache::create_cache_pool, 23 22 }, 24 23 }; 24 + use tokio_util::sync::CancellationToken; 25 25 26 26 use chrono::Duration; 27 27 use smokesignal::config::OAuthBackendConfig; ··· 330 330 331 331 // Create search indexer if enabled 332 332 let search_indexer = if config.enable_opensearch && config.enable_task_opensearch { 333 - let opensearch_endpoint = config.opensearch_endpoint.as_ref().expect( 334 - "OPENSEARCH_ENDPOINT is required when search indexing is enabled", 335 - ); 333 + let opensearch_endpoint = config 334 + .opensearch_endpoint 335 + .as_ref() 336 + .expect("OPENSEARCH_ENDPOINT is required when search indexing is enabled"); 336 337 337 338 match SearchIndexer::new( 338 339 opensearch_endpoint,
+1 -1
src/facets.rs
··· 1207 1207 Facet { 1208 1208 index: ByteSlice { 1209 1209 byte_start: 291, // Should be 290 1210 - byte_end: 324, // Should be 323 - but 324 > text.len() so this facet is SKIPPED 1210 + byte_end: 324, // Should be 323 - but 324 > text.len() so this facet is SKIPPED 1211 1211 }, 1212 1212 features: vec![FacetFeature::Link(Link { 1213 1213 uri: "https://atprotocalls.leaflet.pub/".to_string(),
+6 -8
src/http/acceptance_operations.rs
··· 144 144 145 145 match put_result { 146 146 PutRecordResponse::StrongRef { uri, .. } => Ok((uri, is_update)), 147 - PutRecordResponse::Error(err) => { 148 - Err(AcceptanceError::PutRecordFailed(err.error_message().to_string())) 149 - } 147 + PutRecordResponse::Error(err) => Err(AcceptanceError::PutRecordFailed( 148 + err.error_message().to_string(), 149 + )), 150 150 } 151 151 } 152 152 ··· 232 232 233 233 Ok(()) 234 234 } 235 - atproto_client::com::atproto::repo::DeleteRecordResponse::Error(err) => { 236 - Err(AcceptanceError::DeleteRecordFailed( 237 - err.error_message().to_string(), 238 - )) 239 - } 235 + atproto_client::com::atproto::repo::DeleteRecordResponse::Error(err) => Err( 236 + AcceptanceError::DeleteRecordFailed(err.error_message().to_string()), 237 + ), 240 238 } 241 239 } 242 240
+4 -1
src/http/auth_utils.rs
··· 147 147 let mut conn = match web_context.cache_pool.get().await { 148 148 Ok(conn) => conn, 149 149 Err(e) => { 150 - tracing::debug!(?e, "Failed to get Redis connection for AIP session cache write"); 150 + tracing::debug!( 151 + ?e, 152 + "Failed to get Redis connection for AIP session cache write" 153 + ); 151 154 return; 152 155 } 153 156 };
+2 -2
src/http/errors/mod.rs
··· 9 9 pub mod import_error; 10 10 pub mod lfg_error; 11 11 pub mod login_error; 12 - pub mod profile_import_error; 13 12 pub mod middleware_errors; 13 + pub mod profile_import_error; 14 14 pub mod url_error; 15 15 pub mod view_event_error; 16 16 pub mod web_error; ··· 22 22 pub(crate) use import_error::ImportError; 23 23 pub(crate) use lfg_error::LfgError; 24 24 pub(crate) use login_error::LoginError; 25 - pub(crate) use profile_import_error::ProfileImportError; 26 25 pub(crate) use middleware_errors::WebSessionError; 26 + pub(crate) use profile_import_error::ProfileImportError; 27 27 pub(crate) use url_error::UrlError; 28 28 pub(crate) use view_event_error::ViewEventError; 29 29 pub(crate) use web_error::WebError;
+3 -1
src/http/errors/web_error.rs
··· 166 166 /// The contained string is the destination URL to redirect to after re-authentication. 167 167 /// 168 168 /// **Error Code:** `error-smokesignal-web-3` 169 - #[error("error-smokesignal-web-3 Session expired, re-authentication required (destination: {0})")] 169 + #[error( 170 + "error-smokesignal-web-3 Session expired, re-authentication required (destination: {0})" 171 + )] 170 172 SessionStale(String), 171 173 172 174 /// An internal server error occurred.
+1 -1
src/http/event_validation.rs
··· 11 11 use thiserror::Error; 12 12 13 13 use atproto_identity::resolve::IdentityResolver; 14 + use atproto_record::lexicon::TypedBlob; 14 15 use atproto_record::lexicon::app::bsky::richtext::facet::Facet; 15 16 use atproto_record::lexicon::community::lexicon::calendar::event::{ 16 17 AspectRatio, EventLink, Media, Mode, Status, TypedEventLink, 17 18 }; 18 19 use atproto_record::lexicon::community::lexicon::location::{Address, Geo, LocationOrRef}; 19 - use atproto_record::lexicon::TypedBlob; 20 20 use atproto_record::typed::TypedLexicon; 21 21 22 22 use crate::atproto::utils::{location_from_address, location_from_geo};
+6 -3
src/http/event_view.rs
··· 227 227 .collect(); 228 228 229 229 // Collect structured link URIs for deduplication 230 - let structured_link_uris: HashSet<&str> = 231 - structured_links.iter().map(|(uri, _)| uri.as_str()).collect(); 230 + let structured_link_uris: HashSet<&str> = structured_links 231 + .iter() 232 + .map(|(uri, _)| uri.as_str()) 233 + .collect(); 232 234 233 235 // Extract facets from the event record and render HTML description 234 236 let (description_html, description_links, description_tags) = if let Some(desc_text) = ··· 455 457 header: header_image.clone(), 456 458 header_cid: header_image.map(|(cid, _)| cid), 457 459 thumbnail_cid: find_thumbnail(event).map(|(cid, _)| cid), 458 - thumbnail_alt: find_thumbnail(event).and_then(|(_, alt)| if alt.is_empty() { None } else { Some(alt) }), 460 + thumbnail_alt: find_thumbnail(event) 461 + .and_then(|(_, alt)| if alt.is_empty() { None } else { Some(alt) }), 459 462 require_confirmed_email: event.require_confirmed_email, 460 463 disable_direct_rsvp: event.disable_direct_rsvp, 461 464 rsvp_redirect_url: event.rsvp_redirect_url.clone(),
+8 -6
src/http/h3_utils.rs
··· 34 34 lon: f64, 35 35 resolution: Resolution, 36 36 ) -> Result<String, String> { 37 - let coord = 38 - LatLng::new(lat, lon).map_err(|e| format!("Invalid coordinates: {}", e))?; 37 + let coord = LatLng::new(lat, lon).map_err(|e| format!("Invalid coordinates: {}", e))?; 39 38 let cell = coord.to_cell(resolution); 40 39 Ok(cell.to_string()) 41 40 } ··· 142 141 143 142 if current_resolution > target_resolution { 144 143 // Cell is at finer resolution, get parent at target resolution 145 - cell.parent(target_resolution) 146 - .ok_or_else(|| format!("Failed to get parent cell at resolution {:?}", target_resolution)) 144 + cell.parent(target_resolution).ok_or_else(|| { 145 + format!( 146 + "Failed to get parent cell at resolution {:?}", 147 + target_resolution 148 + ) 149 + }) 147 150 } else { 148 151 // Cell is already at or coarser than target resolution 149 152 Ok(cell) ··· 272 275 273 276 // The normalized cell should contain the original cell's location 274 277 let (lat, lon) = h3_to_lat_lon(&h3_res7).unwrap(); 275 - let expected_parent = 276 - lat_lon_to_h3_with_resolution(lat, lon, Resolution::Six).unwrap(); 278 + let expected_parent = lat_lon_to_h3_with_resolution(lat, lon, Resolution::Six).unwrap(); 277 279 assert_eq!(normalized.to_string(), expected_parent); 278 280 } 279 281
+2 -2
src/http/handle_accept_rsvp.rs
··· 21 21 format_error_html, format_success_html, send_acceptance_email_notification, 22 22 verify_event_organizer_authorization, 23 23 }, 24 - auth_utils::{require_valid_aip_session, AipSessionStatus}, 24 + auth_utils::{AipSessionStatus, require_valid_aip_session}, 25 25 context::WebContext, 26 - errors::{acceptance_error::AcceptanceError, WebError}, 26 + errors::{WebError, acceptance_error::AcceptanceError}, 27 27 middleware_auth::Auth, 28 28 middleware_i18n::Language, 29 29 },
+5 -5
src/http/handle_admin_event_index.rs
··· 140 140 params.push(("query", q.clone())); 141 141 } 142 142 143 - let params_refs: Vec<(&str, &str)> = params 144 - .iter() 145 - .map(|(k, v)| (*k, v.as_str())) 146 - .collect(); 143 + let params_refs: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect(); 147 144 148 145 let pagination_view = PaginationView::new(page_size, events.len() as i64, page, params_refs); 149 146 ··· 266 263 } 267 264 }; 268 265 269 - tracing::info!("Event search index rebuilt: {} events indexed", indexed_count); 266 + tracing::info!( 267 + "Event search index rebuilt: {} events indexed", 268 + indexed_count 269 + ); 270 270 271 271 Ok(Redirect::to("/admin/search-index/events").into_response()) 272 272 }
+2 -1
src/http/handle_admin_import_event.rs
··· 13 13 14 14 use crate::{ 15 15 http::{ 16 - context::AdminRequestContext, errors::WebError, 16 + context::AdminRequestContext, 17 + errors::WebError, 17 18 import_utils::{import_event_records, import_from_aturi}, 18 19 }, 19 20 storage::identity_profile::handle_warm_up,
+7 -6
src/http/handle_admin_profile_index.rs
··· 39 39 "https://{}/admin/search-index/profiles", 40 40 admin_ctx.web_context.config.external_base 41 41 ); 42 - let default_context = admin_template_context(&admin_ctx, &canonical_url, "search-index-profiles"); 42 + let default_context = 43 + admin_template_context(&admin_ctx, &canonical_url, "search-index-profiles"); 43 44 44 45 let render_template = select_template!("admin_profile_index", false, false, admin_ctx.language); 45 46 let error_template = select_template!(false, false, admin_ctx.language); ··· 140 141 params.push(("query", q.clone())); 141 142 } 142 143 143 - let params_refs: Vec<(&str, &str)> = params 144 - .iter() 145 - .map(|(k, v)| (*k, v.as_str())) 146 - .collect(); 144 + let params_refs: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect(); 147 145 148 146 let pagination_view = PaginationView::new(page_size, profiles.len() as i64, page, params_refs); 149 147 ··· 266 264 } 267 265 }; 268 266 269 - tracing::info!("Profile search index rebuilt: {} profiles indexed", indexed_count); 267 + tracing::info!( 268 + "Profile search index rebuilt: {} profiles indexed", 269 + indexed_count 270 + ); 270 271 271 272 Ok(Redirect::to("/admin/search-index/profiles").into_response()) 272 273 }
+50 -8
src/http/handle_blob.rs
··· 19 19 config::OAuthBackendConfig, 20 20 contextual_error, 21 21 http::{ 22 - auth_utils::{require_valid_aip_session, AipSessionStatus}, 22 + auth_utils::{AipSessionStatus, require_valid_aip_session}, 23 23 context::WebContext, 24 - errors::{blob_error::BlobError, CommonError, WebError}, 24 + errors::{CommonError, WebError, blob_error::BlobError}, 25 25 middleware_auth::Auth, 26 26 middleware_i18n::Language, 27 27 }, ··· 416 416 let put_record_url = format!("{}/xrpc/com.atproto.repo.putRecord", pds_endpoint); 417 417 let record_value = serde_json::to_value(&put_record_request) 418 418 .map_err(|e| BlobError::PutRecordSerializeFailed(e.to_string()))?; 419 - let response = match post_dpop_json(&web_context.http_client, &dpop_auth, &put_record_url, record_value).await { 419 + let response = match post_dpop_json( 420 + &web_context.http_client, 421 + &dpop_auth, 422 + &put_record_url, 423 + record_value, 424 + ) 425 + .await 426 + { 420 427 Ok(response) => response, 421 428 Err(e) => { 422 429 let err_string = e.to_string(); 423 430 if err_string.contains("InvalidSwap") { 424 431 let default_context = minijinja::context! { language => language.to_string(), current_handle => current_handle.clone() }; 425 - return contextual_error!(web_context, language, error_template, default_context, CommonError::InvalidSwap, StatusCode::CONFLICT); 432 + return contextual_error!( 433 + web_context, 434 + language, 435 + error_template, 436 + default_context, 437 + CommonError::InvalidSwap, 438 + StatusCode::CONFLICT 439 + ); 426 440 } 427 441 return Err(BlobError::PutRecordFailed(e.to_string()).into()); 428 442 } ··· 533 547 let put_record_url = format!("{}/xrpc/com.atproto.repo.putRecord", pds_endpoint); 534 548 let record_value = serde_json::to_value(&put_record_request) 535 549 .map_err(|e| BlobError::PutRecordSerializeFailed(e.to_string()))?; 536 - let response = match post_dpop_json(&web_context.http_client, &dpop_auth, &put_record_url, record_value).await { 550 + let response = match post_dpop_json( 551 + &web_context.http_client, 552 + &dpop_auth, 553 + &put_record_url, 554 + record_value, 555 + ) 556 + .await 557 + { 537 558 Ok(response) => response, 538 559 Err(e) => { 539 560 let err_string = e.to_string(); 540 561 if err_string.contains("InvalidSwap") { 541 562 let default_context = minijinja::context! { language => language.to_string(), current_handle => current_handle.clone() }; 542 - return contextual_error!(web_context, language, error_template, default_context, CommonError::InvalidSwap, StatusCode::CONFLICT); 563 + return contextual_error!( 564 + web_context, 565 + language, 566 + error_template, 567 + default_context, 568 + CommonError::InvalidSwap, 569 + StatusCode::CONFLICT 570 + ); 543 571 } 544 572 return Err(BlobError::PutRecordFailed(e.to_string()).into()); 545 573 } ··· 649 677 let put_record_url = format!("{}/xrpc/com.atproto.repo.putRecord", pds_endpoint); 650 678 let record_value = serde_json::to_value(&put_record_request) 651 679 .map_err(|e| BlobError::PutRecordSerializeFailed(e.to_string()))?; 652 - let response = match post_dpop_json(&web_context.http_client, &dpop_auth, &put_record_url, record_value).await { 680 + let response = match post_dpop_json( 681 + &web_context.http_client, 682 + &dpop_auth, 683 + &put_record_url, 684 + record_value, 685 + ) 686 + .await 687 + { 653 688 Ok(response) => response, 654 689 Err(e) => { 655 690 let err_string = e.to_string(); 656 691 if err_string.contains("InvalidSwap") { 657 692 let default_context = minijinja::context! { language => language.to_string(), current_handle => current_handle.clone() }; 658 - return contextual_error!(web_context, language, error_template, default_context, CommonError::InvalidSwap, StatusCode::CONFLICT); 693 + return contextual_error!( 694 + web_context, 695 + language, 696 + error_template, 697 + default_context, 698 + CommonError::InvalidSwap, 699 + StatusCode::CONFLICT 700 + ); 659 701 } 660 702 return Err(BlobError::PutRecordFailed(e.to_string()).into()); 661 703 }
+5 -2
src/http/handle_bulk_accept_rsvps.rs
··· 17 17 format_error_html, format_success_html, send_acceptance_email_notification, 18 18 verify_event_organizer_authorization, 19 19 }, 20 - auth_utils::{require_valid_aip_session, AipSessionStatus}, 20 + auth_utils::{AipSessionStatus, require_valid_aip_session}, 21 21 context::WebContext, 22 22 errors::WebError, 23 23 middleware_auth::Auth, ··· 215 215 verify_event_organizer_authorization(web_context, &rsvp.event_aturi, current_did).await?; 216 216 217 217 // Check AIP session validity before attempting AT Protocol operation 218 - if let AipSessionStatus::Stale = require_valid_aip_session(web_context, auth).await.map_err(|e| anyhow::anyhow!("{}", e))? { 218 + if let AipSessionStatus::Stale = require_valid_aip_session(web_context, auth) 219 + .await 220 + .map_err(|e| anyhow::anyhow!("{}", e))? 221 + { 219 222 return Err(anyhow::anyhow!("Session expired, please log in again")); 220 223 } 221 224
+18 -16
src/http/handle_create_event.rs
··· 17 17 create_dpop_auth_from_aip_session, create_dpop_auth_from_oauth_session, 18 18 }; 19 19 use crate::config::OAuthBackendConfig; 20 - use crate::http::auth_utils::{require_valid_aip_session, AipSessionStatus}; 20 + use crate::http::auth_utils::{AipSessionStatus, require_valid_aip_session}; 21 21 use crate::http::context::WebContext; 22 22 use crate::http::errors::CommonError; 23 23 use crate::http::errors::WebError; ··· 159 159 query.starts_at.clone() 160 160 } else { 161 161 let now = Utc::now(); 162 - let parsed_tz = default_tz.parse::<chrono_tz::Tz>().unwrap_or(chrono_tz::UTC); 162 + let parsed_tz = default_tz 163 + .parse::<chrono_tz::Tz>() 164 + .unwrap_or(chrono_tz::UTC); 163 165 let local_date = now.with_timezone(&parsed_tz).date_naive(); 164 166 165 167 local_date ··· 274 276 create_dpop_auth_from_oauth_session(session)? 275 277 } 276 278 (Auth::Aip { access_token, .. }, OAuthBackendConfig::AIP { hostname, .. }) => { 277 - create_dpop_auth_from_aip_session( 278 - &web_context.http_client, 279 - hostname, 280 - access_token, 281 - ) 282 - .await? 279 + create_dpop_auth_from_aip_session(&web_context.http_client, hostname, access_token) 280 + .await? 283 281 } 284 282 _ => return Err(CommonError::NotAuthorized.into()), 285 283 }; ··· 329 327 StatusCode::INTERNAL_SERVER_ERROR, 330 328 Json(json!({ 331 329 "error": err.error_message() 332 - })) 333 - ).into_response()); 330 + })), 331 + ) 332 + .into_response()); 334 333 } 335 334 Err(err) => { 336 335 return Ok(( 337 336 StatusCode::INTERNAL_SERVER_ERROR, 338 337 Json(json!({ 339 338 "error": err.to_string() 340 - })) 341 - ).into_response()); 339 + })), 340 + ) 341 + .into_response()); 342 342 } 343 343 }; 344 344 ··· 364 364 StatusCode::INTERNAL_SERVER_ERROR, 365 365 Json(json!({ 366 366 "error": err.to_string() 367 - })) 368 - ).into_response()); 367 + })), 368 + ) 369 + .into_response()); 369 370 } 370 371 371 372 // Download and store header image from PDS if one was uploaded ··· 401 402 "uri": create_record_response.uri, 402 403 "cid": create_record_response.cid, 403 404 "url": event_url 404 - })) 405 - ).into_response()) 405 + })), 406 + ) 407 + .into_response()) 406 408 }
+4 -2
src/http/handle_create_rsvp.rs
··· 19 19 use crate::{ 20 20 contextual_error, 21 21 http::{ 22 - auth_utils::{require_valid_aip_session, AipSessionStatus}, 22 + auth_utils::{AipSessionStatus, require_valid_aip_session}, 23 23 context::WebContext, 24 24 errors::{CommonError, CreateRsvpError, WebError}, 25 25 middleware_auth::Auth, ··· 150 150 let now = Utc::now(); 151 151 152 152 // Check AIP session validity before attempting AT Protocol operation 153 - if let AipSessionStatus::Stale = require_valid_aip_session(&web_context, &auth).await? { 153 + if let AipSessionStatus::Stale = 154 + require_valid_aip_session(&web_context, &auth).await? 155 + { 154 156 return Err(WebError::SessionStale("/rsvp".to_string())); 155 157 } 156 158
+4 -2
src/http/handle_delete_event.rs
··· 13 13 config::OAuthBackendConfig, 14 14 contextual_error, 15 15 http::{ 16 - auth_utils::{require_valid_aip_session, AipSessionStatus}, 16 + auth_utils::{AipSessionStatus, require_valid_aip_session}, 17 17 context::UserRequestContext, 18 18 errors::{DeleteEventError, WebError}, 19 19 middleware_auth::Auth, ··· 82 82 // If form is submitted with confirmation or it's a non-HTMX POST, proceed with deletion 83 83 if form.confirm.as_deref() == Some("true") { 84 84 // Check AIP session validity before attempting AT Protocol operation 85 - if let AipSessionStatus::Stale = require_valid_aip_session(&ctx.web_context, &ctx.auth).await? { 85 + if let AipSessionStatus::Stale = 86 + require_valid_aip_session(&ctx.web_context, &ctx.auth).await? 87 + { 86 88 return Err(WebError::SessionStale("/".to_string())); 87 89 } 88 90
+38 -31
src/http/handle_edit_event.rs
··· 5 5 use crate::atproto::auth::{ 6 6 create_dpop_auth_from_aip_session, create_dpop_auth_from_oauth_session, 7 7 }; 8 - use crate::http::auth_utils::{require_valid_aip_session, AipSessionStatus}; 8 + use crate::http::auth_utils::{AipSessionStatus, require_valid_aip_session}; 9 9 use crate::http::context::UserRequestContext; 10 10 use crate::http::errors::{CommonError, WebError}; 11 11 use crate::http::event_form::ManageEvent; ··· 63 63 if profile.did != current_handle.did { 64 64 return Ok(( 65 65 StatusCode::FORBIDDEN, 66 - Json(json!({"error": "Not authorized"})) 67 - ).into_response()); 66 + Json(json!({"error": "Not authorized"})), 67 + ) 68 + .into_response()); 68 69 } 69 70 70 71 // Get existing event ··· 73 74 Err(_) => { 74 75 return Ok(( 75 76 StatusCode::NOT_FOUND, 76 - Json(json!({"error": "Event not found"})) 77 - ).into_response()); 77 + Json(json!({"error": "Event not found"})), 78 + ) 79 + .into_response()); 78 80 } 79 81 }; 80 82 81 83 // Parse existing event record to get created_at 82 - let existing_event = match serde_json::from_value::<LexiconCommunityEvent>(event.record.0.clone()) { 83 - Ok(evt) => evt, 84 - Err(_) => { 85 - return Ok(( 86 - StatusCode::INTERNAL_SERVER_ERROR, 87 - Json(json!({"error": "Invalid event format"})) 88 - ).into_response()); 89 - } 90 - }; 84 + let existing_event = 85 + match serde_json::from_value::<LexiconCommunityEvent>(event.record.0.clone()) { 86 + Ok(evt) => evt, 87 + Err(_) => { 88 + return Ok(( 89 + StatusCode::INTERNAL_SERVER_ERROR, 90 + Json(json!({"error": "Invalid event format"})), 91 + ) 92 + .into_response()); 93 + } 94 + }; 91 95 92 96 // Validate and parse the complete request using shared validation 93 97 let facet_limits = crate::facets::FacetLimits { ··· 119 123 (Auth::Pds { session, .. }, crate::config::OAuthBackendConfig::ATProtocol { .. }) => { 120 124 create_dpop_auth_from_oauth_session(session)? 121 125 } 122 - (Auth::Aip { access_token, .. }, crate::config::OAuthBackendConfig::AIP { hostname, .. }) => { 123 - create_dpop_auth_from_aip_session( 124 - &ctx.web_context.http_client, 125 - hostname, 126 - access_token, 127 - ) 128 - .await? 126 + ( 127 + Auth::Aip { access_token, .. }, 128 + crate::config::OAuthBackendConfig::AIP { hostname, .. }, 129 + ) => { 130 + create_dpop_auth_from_aip_session(&ctx.web_context.http_client, hostname, access_token) 131 + .await? 129 132 } 130 133 _ => return Err(CommonError::NotAuthorized.into()), 131 134 }; ··· 134 137 let the_record = LexiconCommunityEvent { 135 138 name: validated.name.clone(), 136 139 description: validated.description.clone(), 137 - created_at: existing_event.created_at, // Preserve original creation time 140 + created_at: existing_event.created_at, // Preserve original creation time 138 141 starts_at: validated.starts_at, 139 142 ends_at: validated.ends_at, 140 143 mode: validated.mode, ··· 143 146 uris: validated.uris, 144 147 media: validated.media, 145 148 facets: validated.facets, 146 - extra: existing_event.extra.clone(), // Preserve any extra fields 149 + extra: existing_event.extra.clone(), // Preserve any extra fields 147 150 }; 148 151 149 152 // Submit to AT Protocol ··· 172 175 Ok(PutRecordResponse::Error(err)) => { 173 176 return Ok(( 174 177 StatusCode::INTERNAL_SERVER_ERROR, 175 - Json(json!({"error": err.error_message()})) 176 - ).into_response()); 178 + Json(json!({"error": err.error_message()})), 179 + ) 180 + .into_response()); 177 181 } 178 182 Err(err) => { 179 183 return Ok(( 180 184 StatusCode::INTERNAL_SERVER_ERROR, 181 - Json(json!({"error": err.to_string()})) 182 - ).into_response()); 185 + Json(json!({"error": err.to_string()})), 186 + ) 187 + .into_response()); 183 188 } 184 189 }; 185 190 ··· 214 219 if let Err(err) = event_update_result { 215 220 return Ok(( 216 221 StatusCode::INTERNAL_SERVER_ERROR, 217 - Json(json!({"error": err.to_string()})) 218 - ).into_response()); 222 + Json(json!({"error": err.to_string()})), 223 + ) 224 + .into_response()); 219 225 } 220 226 221 227 // Re-index the event in OpenSearch to update locations_geo and other fields ··· 359 365 "uri": put_record_response.uri, 360 366 "cid": put_record_response.cid, 361 367 "url": event_url 362 - })) 363 - ).into_response()) 368 + })), 369 + ) 370 + .into_response()) 364 371 }
+1 -1
src/http/handle_edit_settings.rs
··· 11 11 http::errors::{CommonError, WebError}, 12 12 select_template, 13 13 storage::{ 14 - event::{event_get, event_update_with_full_metadata, EventUpdateParams}, 14 + event::{EventUpdateParams, event_get, event_update_with_full_metadata}, 15 15 identity_profile::{handle_for_did, handle_for_handle}, 16 16 }, 17 17 };
+8 -9
src/http/handle_finalize_acceptance.rs
··· 18 18 config::OAuthBackendConfig, 19 19 http::{ 20 20 acceptance_utils::format_success_html, 21 - auth_utils::{require_valid_aip_session, AipSessionStatus}, 21 + auth_utils::{AipSessionStatus, require_valid_aip_session}, 22 22 context::WebContext, 23 - errors::{acceptance_error::AcceptanceError, WebError}, 23 + errors::{WebError, acceptance_error::AcceptanceError}, 24 24 middleware_auth::Auth, 25 25 middleware_i18n::Language, 26 26 }, ··· 29 29 acceptance_ticket_delete_by_event_and_rsvp_did, acceptance_ticket_get, 30 30 acceptance_ticket_get_by_event_and_rsvp_did, rsvp_update_validated_at, 31 31 }, 32 - event::{event_get, rsvp_get_by_event_and_did, rsvp_insert_with_metadata, RsvpInsertParams}, 32 + event::{ 33 + RsvpInsertParams, event_get, rsvp_get_by_event_and_did, rsvp_insert_with_metadata, 34 + }, 33 35 }, 34 36 }; 35 37 use atproto_client::com::atproto::repo::{PutRecordRequest, PutRecordResponse, put_record}; ··· 136 138 extra: Default::default(), 137 139 }; 138 140 139 - let rsvp_aturi = format!( 140 - "at://{}/{}/{}", 141 - current_handle.did, RSVP_NSID, record_key 142 - ); 141 + let rsvp_aturi = format!("at://{}/{}/{}", current_handle.did, RSVP_NSID, record_key); 143 142 144 143 // Convert Rsvp to Value for consistent return type 145 144 let rsvp_value = serde_json::to_value(&new_rsvp) ··· 253 252 }; 254 253 255 254 // Parse the RSVP AT-URI to extract the record key 256 - let parsed_rsvp_aturi = 257 - ATURI::from_str(&rsvp_aturi).map_err(|e| AcceptanceError::InvalidRsvpAtUri(e.to_string()))?; 255 + let parsed_rsvp_aturi = ATURI::from_str(&rsvp_aturi) 256 + .map_err(|e| AcceptanceError::InvalidRsvpAtUri(e.to_string()))?; 258 257 259 258 // Update/Create the RSVP in the user's PDS 260 259 let put_record_request = PutRecordRequest {
+72 -31
src/http/handle_index.rs
··· 104 104 105 105 #[derive(Serialize)] 106 106 pub struct DateGroup { 107 - pub date: String, // e.g., "November 10, 2025" 108 - pub date_short: String, // e.g., "Nov 10" 107 + pub date: String, // e.g., "November 10, 2025" 108 + pub date_short: String, // e.g., "Nov 10" 109 109 pub event_groups: Vec<EventGroup>, 110 110 pub lfg_groups: Vec<LfgGroup>, 111 111 } ··· 166 166 } 167 167 168 168 // Limit activities to page size + 1 169 - let activity_list: Vec<_> = activities 170 - .iter() 171 - .take(page_size as usize + 1) 172 - .collect(); 169 + let activity_list: Vec<_> = activities.iter().take(page_size as usize + 1).collect(); 173 170 174 171 // Step 1: Group by date, then by event (for events/RSVPs) or by h3_cell (for LFG) 175 172 // Use BTreeMap with Reverse ordering to get newest dates first 176 - let mut date_event_groups: BTreeMap<std::cmp::Reverse<chrono::NaiveDate>, HashMap<String, Vec<&crate::storage::event::model::ActivityItem>>> = BTreeMap::new(); 177 - let mut date_lfg_groups: BTreeMap<std::cmp::Reverse<chrono::NaiveDate>, HashMap<String, Vec<&crate::storage::event::model::ActivityItem>>> = BTreeMap::new(); 173 + let mut date_event_groups: BTreeMap< 174 + std::cmp::Reverse<chrono::NaiveDate>, 175 + HashMap<String, Vec<&crate::storage::event::model::ActivityItem>>, 176 + > = BTreeMap::new(); 177 + let mut date_lfg_groups: BTreeMap< 178 + std::cmp::Reverse<chrono::NaiveDate>, 179 + HashMap<String, Vec<&crate::storage::event::model::ActivityItem>>, 180 + > = BTreeMap::new(); 178 181 179 182 for activity in &activity_list { 180 183 let date = activity.discovered_at.date_naive(); ··· 213 216 214 217 // Step 3: Build date groups with activities 215 218 // Collect all unique dates from both event and LFG groups 216 - let mut all_dates: std::collections::BTreeSet<std::cmp::Reverse<chrono::NaiveDate>> = std::collections::BTreeSet::new(); 219 + let mut all_dates: std::collections::BTreeSet<std::cmp::Reverse<chrono::NaiveDate>> = 220 + std::collections::BTreeSet::new(); 217 221 for key in date_event_groups.keys() { 218 222 all_dates.insert(*key); 219 223 } ··· 287 291 288 292 // Don't show status for grouped RSVPs 289 293 // Use the newest RSVP's timestamp (created_at if available, else discovered_at) 290 - let newest_timestamp = first_activity.created_at.unwrap_or(first_activity.discovered_at); 294 + let newest_timestamp = first_activity 295 + .created_at 296 + .unwrap_or(first_activity.discovered_at); 291 297 activity_displays.push(ActivityDisplay::GroupedRsvp { 292 298 handles: group_handles, 293 299 dids, ··· 372 378 // Sort activity displays by timestamp, newest first 373 379 activity_displays.sort_by(|a, b| { 374 380 let a_time = match a { 375 - ActivityDisplay::Individual(view) => chrono::DateTime::parse_from_rfc3339(&view.updated_at).ok(), 376 - ActivityDisplay::GroupedRsvp { updated_at, .. } => chrono::DateTime::parse_from_rfc3339(updated_at).ok(), 377 - ActivityDisplay::GroupedLfg { updated_at, .. } => chrono::DateTime::parse_from_rfc3339(updated_at).ok(), 381 + ActivityDisplay::Individual(view) => { 382 + chrono::DateTime::parse_from_rfc3339(&view.updated_at).ok() 383 + } 384 + ActivityDisplay::GroupedRsvp { updated_at, .. } => { 385 + chrono::DateTime::parse_from_rfc3339(updated_at).ok() 386 + } 387 + ActivityDisplay::GroupedLfg { updated_at, .. } => { 388 + chrono::DateTime::parse_from_rfc3339(updated_at).ok() 389 + } 378 390 }; 379 391 let b_time = match b { 380 - ActivityDisplay::Individual(view) => chrono::DateTime::parse_from_rfc3339(&view.updated_at).ok(), 381 - ActivityDisplay::GroupedRsvp { updated_at, .. } => chrono::DateTime::parse_from_rfc3339(updated_at).ok(), 382 - ActivityDisplay::GroupedLfg { updated_at, .. } => chrono::DateTime::parse_from_rfc3339(updated_at).ok(), 392 + ActivityDisplay::Individual(view) => { 393 + chrono::DateTime::parse_from_rfc3339(&view.updated_at).ok() 394 + } 395 + ActivityDisplay::GroupedRsvp { updated_at, .. } => { 396 + chrono::DateTime::parse_from_rfc3339(updated_at).ok() 397 + } 398 + ActivityDisplay::GroupedLfg { updated_at, .. } => { 399 + chrono::DateTime::parse_from_rfc3339(updated_at).ok() 400 + } 383 401 }; 384 402 b_time.cmp(&a_time) // Descending order: newer timestamps first 385 403 }); ··· 410 428 if !event_groups.is_empty() { 411 429 event_groups.sort_by(|a, b| { 412 430 let a_time = a.activities.first().and_then(|act| match act { 413 - ActivityDisplay::Individual(view) => chrono::DateTime::parse_from_rfc3339(&view.updated_at).ok(), 414 - ActivityDisplay::GroupedRsvp { updated_at, .. } => chrono::DateTime::parse_from_rfc3339(updated_at).ok(), 415 - ActivityDisplay::GroupedLfg { updated_at, .. } => chrono::DateTime::parse_from_rfc3339(updated_at).ok(), 431 + ActivityDisplay::Individual(view) => { 432 + chrono::DateTime::parse_from_rfc3339(&view.updated_at).ok() 433 + } 434 + ActivityDisplay::GroupedRsvp { updated_at, .. } => { 435 + chrono::DateTime::parse_from_rfc3339(updated_at).ok() 436 + } 437 + ActivityDisplay::GroupedLfg { updated_at, .. } => { 438 + chrono::DateTime::parse_from_rfc3339(updated_at).ok() 439 + } 416 440 }); 417 441 let b_time = b.activities.first().and_then(|act| match act { 418 - ActivityDisplay::Individual(view) => chrono::DateTime::parse_from_rfc3339(&view.updated_at).ok(), 419 - ActivityDisplay::GroupedRsvp { updated_at, .. } => chrono::DateTime::parse_from_rfc3339(updated_at).ok(), 420 - ActivityDisplay::GroupedLfg { updated_at, .. } => chrono::DateTime::parse_from_rfc3339(updated_at).ok(), 442 + ActivityDisplay::Individual(view) => { 443 + chrono::DateTime::parse_from_rfc3339(&view.updated_at).ok() 444 + } 445 + ActivityDisplay::GroupedRsvp { updated_at, .. } => { 446 + chrono::DateTime::parse_from_rfc3339(updated_at).ok() 447 + } 448 + ActivityDisplay::GroupedLfg { updated_at, .. } => { 449 + chrono::DateTime::parse_from_rfc3339(updated_at).ok() 450 + } 421 451 }); 422 452 b_time.cmp(&a_time) // Descending order: newer timestamps first 423 453 }); ··· 458 488 .collect(); 459 489 460 490 let first_activity = sorted_activities[0]; 461 - let newest_timestamp = first_activity.created_at.unwrap_or(first_activity.discovered_at); 491 + let newest_timestamp = first_activity 492 + .created_at 493 + .unwrap_or(first_activity.discovered_at); 462 494 463 495 lfg_activity_displays.push(ActivityDisplay::GroupedLfg { 464 496 handles: group_handles, ··· 505 537 if !lfg_groups.is_empty() { 506 538 lfg_groups.sort_by(|a, b| { 507 539 let a_time = a.activities.first().and_then(|act| match act { 508 - ActivityDisplay::Individual(view) => chrono::DateTime::parse_from_rfc3339(&view.updated_at).ok(), 509 - ActivityDisplay::GroupedLfg { updated_at, .. } => chrono::DateTime::parse_from_rfc3339(updated_at).ok(), 540 + ActivityDisplay::Individual(view) => { 541 + chrono::DateTime::parse_from_rfc3339(&view.updated_at).ok() 542 + } 543 + ActivityDisplay::GroupedLfg { updated_at, .. } => { 544 + chrono::DateTime::parse_from_rfc3339(updated_at).ok() 545 + } 510 546 _ => None, 511 547 }); 512 548 let b_time = b.activities.first().and_then(|act| match act { 513 - ActivityDisplay::Individual(view) => chrono::DateTime::parse_from_rfc3339(&view.updated_at).ok(), 514 - ActivityDisplay::GroupedLfg { updated_at, .. } => chrono::DateTime::parse_from_rfc3339(updated_at).ok(), 549 + ActivityDisplay::Individual(view) => { 550 + chrono::DateTime::parse_from_rfc3339(&view.updated_at).ok() 551 + } 552 + ActivityDisplay::GroupedLfg { updated_at, .. } => { 553 + chrono::DateTime::parse_from_rfc3339(updated_at).ok() 554 + } 515 555 _ => None, 516 556 }); 517 557 b_time.cmp(&a_time) ··· 541 581 }) 542 582 .sum(); 543 583 544 - let pagination_view = 545 - PaginationView::new(page_size, total_activities as i64, page, params); 584 + let pagination_view = PaginationView::new(page_size, total_activities as i64, page, params); 546 585 547 586 // Truncate if we have more than page_size activities 548 587 if total_activities > page_size as usize { ··· 597 636 598 637 let event_count_formatted = crate::stats::NetworkStats::format_number(stats.event_count); 599 638 let rsvp_count_formatted = crate::stats::NetworkStats::format_number(stats.rsvp_count); 600 - let lfg_identities_count_formatted = crate::stats::NetworkStats::format_number(stats.lfg_identities_count); 601 - let lfg_locations_count_formatted = crate::stats::NetworkStats::format_number(stats.lfg_locations_count); 639 + let lfg_identities_count_formatted = 640 + crate::stats::NetworkStats::format_number(stats.lfg_identities_count); 641 + let lfg_locations_count_formatted = 642 + crate::stats::NetworkStats::format_number(stats.lfg_locations_count); 602 643 603 644 Ok(( 604 645 http::StatusCode::OK,
+13 -20
src/http/handle_lfg.rs
··· 31 31 use crate::http::context::WebContext; 32 32 use crate::http::errors::{CommonError, LfgError, WebError}; 33 33 use crate::http::event_view::EventView; 34 - use crate::http::lfg_form::{ALLOWED_DURATIONS, DEFAULT_DURATION_HOURS, MAX_TAGS, MAX_TAG_LENGTH}; 34 + use crate::http::lfg_form::{ALLOWED_DURATIONS, DEFAULT_DURATION_HOURS, MAX_TAG_LENGTH, MAX_TAGS}; 35 35 use crate::http::middleware_auth::Auth; 36 36 use crate::http::middleware_i18n::Language; 37 37 use crate::search_index::{GeoCenter, IndexedEvent, IndexedLfgProfile, SearchIndexManager}; 38 38 use crate::select_template; 39 + use crate::storage::StoragePool; 39 40 use crate::storage::atproto_record::atproto_record_upsert; 40 41 use crate::storage::event::event_get; 41 42 use crate::storage::identity_profile::{handle_for_did, handles_by_did}; 42 43 use crate::storage::lfg::{lfg_get_active_by_did, lfg_get_all_by_did}; 43 44 use crate::storage::profile::profile_get_by_did; 44 - use crate::storage::StoragePool; 45 45 46 46 // ============================================================================ 47 47 // Request/Response Types ··· 169 169 .unwrap_or_default() 170 170 } 171 171 172 - 173 172 /// Serializable LFG profile for template display 174 173 #[derive(Debug, Serialize)] 175 174 struct TemplateProfile { ··· 199 198 } 200 199 201 200 /// Enrich LFG profiles with display_name and handle from the database. 202 - async fn enrich_profiles(pool: &StoragePool, profiles: Vec<IndexedLfgProfile>) -> Vec<TemplateProfile> { 201 + async fn enrich_profiles( 202 + pool: &StoragePool, 203 + profiles: Vec<IndexedLfgProfile>, 204 + ) -> Vec<TemplateProfile> { 203 205 let mut enriched = Vec::with_capacity(profiles.len()); 204 206 205 207 for p in profiles { ··· 429 431 .iter() 430 432 .filter_map(|event| { 431 433 let organizer = organizer_handles.get(&event.did); 432 - EventView::try_from(( 433 - auth.profile(), 434 - organizer, 435 - event, 436 - &facet_limits, 437 - )) 438 - .ok() 434 + EventView::try_from((auth.profile(), organizer, event, &facet_limits)).ok() 439 435 }) 440 436 .collect(); 441 437 442 438 // Hydrate RSVP counts 443 - if let Err(err) = crate::http::event_view::hydrate_event_rsvp_counts( 444 - &web_context.pool, 445 - &mut events, 446 - ) 447 - .await 439 + if let Err(err) = 440 + crate::http::event_view::hydrate_event_rsvp_counts(&web_context.pool, &mut events) 441 + .await 448 442 { 449 443 tracing::warn!("Failed to hydrate event RSVP counts: {}", err); 450 444 } ··· 582 576 }; 583 577 584 578 // Store in local database 585 - let record_json = serde_json::to_value(&lfg_record).map_err(|e| { 586 - LfgError::PdsRecordCreationFailed { 579 + let record_json = 580 + serde_json::to_value(&lfg_record).map_err(|e| LfgError::PdsRecordCreationFailed { 587 581 message: e.to_string(), 588 - } 589 - })?; 582 + })?; 590 583 591 584 atproto_record_upsert( 592 585 &web_context.pool,
+1 -7
src/http/handle_location.rs
··· 182 182 let search_manager = match SearchIndexManager::new(opensearch_endpoint) { 183 183 Ok(m) => m, 184 184 Err(err) => { 185 - return contextual_error!( 186 - web_context, 187 - language, 188 - error_template, 189 - default_context, 190 - err 191 - ); 185 + return contextual_error!(web_context, language, error_template, default_context, err); 192 186 } 193 187 }; 194 188
+1 -3
src/http/handle_location_suggestions.rs
··· 9 9 use crate::http::context::WebContext; 10 10 use crate::http::errors::WebError; 11 11 use crate::http::middleware_auth::Auth; 12 - use crate::storage::atproto_record::{ 13 - LocationSuggestion, atproto_record_get_location_suggestions, 14 - }; 12 + use crate::storage::atproto_record::{LocationSuggestion, atproto_record_get_location_suggestions}; 15 13 16 14 /// Response format for location suggestions 17 15 #[derive(Debug, Serialize)]
+12 -9
src/http/handle_manage_event_content.rs
··· 161 161 .into_iter() 162 162 .filter(|(_, status, validated_at)| { 163 163 // Check if this RSVP meets any of the display criteria 164 - display_criteria.iter().any(|criterion| match criterion.as_str() { 165 - "going_confirmed" => { 166 - status == "going" && validated_at.is_some() 167 - } 168 - "going" => status == "going", 169 - "interested" => status == "interested", 170 - _ => false, 171 - }) 164 + display_criteria 165 + .iter() 166 + .any(|criterion| match criterion.as_str() { 167 + "going_confirmed" => { 168 + status == "going" && validated_at.is_some() 169 + } 170 + "going" => status == "going", 171 + "interested" => status == "interested", 172 + _ => false, 173 + }) 172 174 }) 173 175 .collect(); 174 176 ··· 281 283 let private_content_form = crate::http::event_form::PrivateContentFormData { 282 284 private_content: form.private_content.clone(), 283 285 private_content_criteria_going_confirmed: Some( 284 - form.private_content_criteria_going_confirmed.unwrap_or(false), 286 + form.private_content_criteria_going_confirmed 287 + .unwrap_or(false), 285 288 ), 286 289 private_content_criteria_going: Some(form.private_content_criteria_going.unwrap_or(false)), 287 290 private_content_criteria_interested: Some(
+2 -4
src/http/handle_oauth_aip_callback.rs
··· 1 1 use std::{collections::HashMap, sync::Arc}; 2 2 3 3 use crate::{ 4 - atproto::auth::create_dpop_auth_from_aip_session, 5 - config::OAuthBackendConfig, contextual_error, 6 - facets::FacetLimits, 7 - http::handle_email_confirm::send_confirmation_email, select_template, 4 + atproto::auth::create_dpop_auth_from_aip_session, config::OAuthBackendConfig, contextual_error, 5 + facets::FacetLimits, http::handle_email_confirm::send_confirmation_email, select_template, 8 6 storage::identity_profile::handle_warm_up, 9 7 }; 10 8 use anyhow::Result;
+4 -1
src/http/handle_oauth_callback.rs
··· 21 21 use minijinja::context as template_context; 22 22 use serde::{Deserialize, Serialize}; 23 23 24 - use crate::{contextual_error, facets::FacetLimits, select_template, storage::identity_profile::handle_warm_up}; 24 + use crate::{ 25 + contextual_error, facets::FacetLimits, select_template, 26 + storage::identity_profile::handle_warm_up, 27 + }; 25 28 26 29 use super::{ 27 30 context::WebContext,
+4 -8
src/http/handle_preview_description.rs
··· 1 + use axum::Json; 1 2 use axum::extract::State; 2 3 use axum::http::StatusCode; 3 4 use axum::response::IntoResponse; 4 - use axum::Json; 5 5 use axum_extra::extract::Cached; 6 6 use serde::{Deserialize, Serialize}; 7 7 use serde_json::json; 8 8 9 - use crate::facets::{parse_facets_from_text, render_text_with_facets_html, FacetLimits}; 9 + use crate::facets::{FacetLimits, parse_facets_from_text, render_text_with_facets_html}; 10 10 use crate::http::context::WebContext; 11 11 use crate::http::errors::WebError; 12 12 use crate::http::middleware_auth::Auth; ··· 52 52 53 53 // Parse facets from the description 54 54 let limits = FacetLimits::default(); 55 - let facets = parse_facets_from_text( 56 - description, 57 - web_context.identity_resolver.as_ref(), 58 - &limits, 59 - ) 60 - .await; 55 + let facets = 56 + parse_facets_from_text(description, web_context.identity_resolver.as_ref(), &limits).await; 61 57 62 58 // Render HTML with facets 63 59 let html = render_text_with_facets_html(description, facets.as_ref(), &limits);
+3 -1
src/http/handle_profile.rs
··· 366 366 }; 367 367 368 368 // Use created_at if available (from AT Protocol record), otherwise discovered_at 369 - let timestamp = first_activity.created_at.unwrap_or(first_activity.discovered_at); 369 + let timestamp = first_activity 370 + .created_at 371 + .unwrap_or(first_activity.discovered_at); 370 372 activity_displays.push(ActivityDisplay::GroupedRsvp { 371 373 handles: group_handles, 372 374 dids,
+1 -1
src/http/handle_quick_event.rs
··· 6 6 use axum_template::RenderHtml; 7 7 use minijinja::context as template_context; 8 8 9 - use crate::http::auth_utils::{require_valid_aip_session, AipSessionStatus}; 9 + use crate::http::auth_utils::{AipSessionStatus, require_valid_aip_session}; 10 10 use crate::http::context::WebContext; 11 11 use crate::http::errors::WebError; 12 12 use crate::http::middleware_auth::Auth;
+7 -26
src/http/handle_search.rs
··· 24 24 profile_index::ProfileIndexManager, 25 25 search_index::SearchIndexManager, 26 26 select_template, 27 - storage::{ 28 - event::event_get, 29 - identity_profile::handles_by_did, 30 - }, 27 + storage::{event::event_get, identity_profile::handles_by_did}, 31 28 }; 32 29 33 30 #[derive(Debug, Deserialize)] ··· 79 76 language, 80 77 error_template, 81 78 default_context, 82 - anyhow::anyhow!("error-smokesignal-search-1 Search is not available: Search service is not configured") 79 + anyhow::anyhow!( 80 + "error-smokesignal-search-1 Search is not available: Search service is not configured" 81 + ) 83 82 ); 84 83 } 85 84 }; ··· 88 87 let event_manager = match SearchIndexManager::new(opensearch_endpoint) { 89 88 Ok(m) => m, 90 89 Err(err) => { 91 - return contextual_error!( 92 - web_context, 93 - language, 94 - error_template, 95 - default_context, 96 - err 97 - ); 90 + return contextual_error!(web_context, language, error_template, default_context, err); 98 91 } 99 92 }; 100 93 101 94 let profile_manager = match ProfileIndexManager::new(opensearch_endpoint) { 102 95 Ok(m) => m, 103 96 Err(err) => { 104 - return contextual_error!( 105 - web_context, 106 - language, 107 - error_template, 108 - default_context, 109 - err 110 - ); 97 + return contextual_error!(web_context, language, error_template, default_context, err); 111 98 } 112 99 }; 113 100 ··· 216 203 .iter() 217 204 .filter_map(|event| { 218 205 let organizer = organizer_handles.get(&event.did); 219 - EventView::try_from(( 220 - auth.profile(), 221 - organizer, 222 - event, 223 - &facet_limits, 224 - )) 225 - .ok() 206 + EventView::try_from((auth.profile(), organizer, event, &facet_limits)).ok() 226 207 }) 227 208 .collect(); 228 209
+5 -2
src/http/handle_settings.rs
··· 19 19 use crate::{ 20 20 contextual_error, 21 21 http::{ 22 - auth_utils::{require_valid_aip_session, AipSessionStatus}, 23 - context::WebContext, errors::WebError, middleware_auth::Auth, middleware_i18n::Language, 22 + auth_utils::{AipSessionStatus, require_valid_aip_session}, 23 + context::WebContext, 24 + errors::WebError, 25 + middleware_auth::Auth, 26 + middleware_i18n::Language, 24 27 timezones::supported_timezones, 25 28 }, 26 29 select_template,
+1 -1
src/http/handle_unaccept_rsvp.rs
··· 11 11 acceptance_utils::{ 12 12 format_error_html, format_success_html, verify_event_organizer_authorization, 13 13 }, 14 - auth_utils::{require_valid_aip_session, AipSessionStatus}, 14 + auth_utils::{AipSessionStatus, require_valid_aip_session}, 15 15 context::WebContext, 16 16 errors::WebError, 17 17 middleware_auth::Auth,
+125
src/http/handle_xrpc_get_rsvp.rs
··· 1 + use atproto_record::aturi::ATURI; 2 + use axum::{ 3 + extract::{Query, State}, 4 + response::{IntoResponse, Json}, 5 + }; 6 + use http::StatusCode; 7 + use serde::{Deserialize, Serialize}; 8 + use std::str::FromStr; 9 + 10 + use crate::http::context::WebContext; 11 + use crate::storage::event::rsvp_get_by_event_and_did; 12 + 13 + #[derive(Debug, Deserialize)] 14 + pub struct GetRsvpParams { 15 + /// AT-URI of the event 16 + event: String, 17 + /// DID of the identity to get the RSVP for 18 + identity: String, 19 + } 20 + 21 + #[derive(Debug, Serialize)] 22 + pub struct GetRsvpResponse { 23 + /// AT-URI of the RSVP record 24 + uri: String, 25 + /// CID of the RSVP record 26 + cid: String, 27 + /// The RSVP record 28 + record: serde_json::Value, 29 + } 30 + 31 + #[derive(Debug, Serialize)] 32 + struct GetRsvpError { 33 + error: String, 34 + message: String, 35 + } 36 + 37 + impl GetRsvpError { 38 + fn invalid_request(message: &str) -> Self { 39 + Self { 40 + error: "InvalidRequest".to_string(), 41 + message: message.to_string(), 42 + } 43 + } 44 + 45 + fn not_found(message: &str) -> Self { 46 + Self { 47 + error: "NotFound".to_string(), 48 + message: message.to_string(), 49 + } 50 + } 51 + 52 + fn internal_error(message: &str) -> Self { 53 + Self { 54 + error: "InternalServerError".to_string(), 55 + message: message.to_string(), 56 + } 57 + } 58 + } 59 + 60 + /// XRPC query endpoint for getting an RSVP record. 61 + /// 62 + /// This endpoint retrieves an RSVP record for a specific identity and event. 63 + /// No authentication is required as RSVP data is public. 64 + pub(crate) async fn handle_xrpc_get_rsvp( 65 + State(web_context): State<WebContext>, 66 + Query(params): Query<GetRsvpParams>, 67 + ) -> impl IntoResponse { 68 + // Validate event AT-URI format 69 + if ATURI::from_str(&params.event).is_err() { 70 + return ( 71 + StatusCode::BAD_REQUEST, 72 + Json(GetRsvpError::invalid_request("Invalid event AT-URI format")), 73 + ) 74 + .into_response(); 75 + } 76 + 77 + let identity = match web_context 78 + .identity_resolver 79 + .resolve(&params.identity) 80 + .await 81 + { 82 + Ok(value) => value, 83 + Err(_) => { 84 + return ( 85 + StatusCode::BAD_REQUEST, 86 + Json(GetRsvpError::invalid_request("Invalid identity DID format")), 87 + ) 88 + .into_response(); 89 + } 90 + }; 91 + 92 + // Fetch the RSVP 93 + let rsvp = match rsvp_get_by_event_and_did(&web_context.pool, &params.event, &identity.id).await 94 + { 95 + Ok(Some(rsvp)) => rsvp, 96 + Ok(None) => { 97 + return ( 98 + StatusCode::NOT_FOUND, 99 + Json(GetRsvpError::not_found("RSVP not found")), 100 + ) 101 + .into_response(); 102 + } 103 + Err(err) => { 104 + tracing::error!( 105 + ?err, 106 + event = %params.event, 107 + identity = %params.identity, 108 + "Failed to fetch RSVP" 109 + ); 110 + return ( 111 + StatusCode::INTERNAL_SERVER_ERROR, 112 + Json(GetRsvpError::internal_error("Failed to fetch RSVP")), 113 + ) 114 + .into_response(); 115 + } 116 + }; 117 + 118 + let response = GetRsvpResponse { 119 + uri: rsvp.aturi, 120 + cid: rsvp.cid, 121 + record: rsvp.record.0, 122 + }; 123 + 124 + (StatusCode::OK, Json(response)).into_response() 125 + }
+6 -1
src/http/handle_xrpc_search_events.rs
··· 173 173 } else { 174 174 // Search with optional query, DID filter, and location CIDs 175 175 match manager 176 - .search_events(params.query.as_deref(), did.as_deref(), &params.location, limit) 176 + .search_events( 177 + params.query.as_deref(), 178 + did.as_deref(), 179 + &params.location, 180 + limit, 181 + ) 177 182 .await 178 183 { 179 184 Ok(ids) => ids,
+30 -17
src/http/handler_mcp.rs
··· 1 1 use axum::{ 2 2 extract::State, 3 - http::{header, HeaderMap, StatusCode}, 3 + http::{HeaderMap, StatusCode, header}, 4 4 response::{IntoResponse, Json, Response}, 5 5 }; 6 6 use serde::{Deserialize, Serialize}; 7 - use serde_json::{json, Value}; 7 + use serde_json::{Value, json}; 8 8 9 9 use crate::aip_auth_cache::AipUserInfo; 10 10 use crate::http::context::WebContext; ··· 96 96 ); 97 97 98 98 let mut headers = HeaderMap::new(); 99 - headers.insert( 100 - header::WWW_AUTHENTICATE, 101 - www_authenticate.parse().unwrap(), 102 - ); 99 + headers.insert(header::WWW_AUTHENTICATE, www_authenticate.parse().unwrap()); 103 100 104 101 (StatusCode::UNAUTHORIZED, headers, "Unauthorized").into_response() 105 102 } ··· 362 359 let params = &request.params; 363 360 tracing::debug!(params = ?params, "Tool call params"); 364 361 365 - let tool_name = params 366 - .get("name") 367 - .and_then(|v| v.as_str()) 368 - .unwrap_or(""); 362 + let tool_name = params.get("name").and_then(|v| v.as_str()).unwrap_or(""); 369 363 370 364 let arguments = params.get("arguments").unwrap_or(&Value::Null); 371 365 ··· 399 393 "Calling get_event tool" 400 394 ); 401 395 402 - match tools::get_event(web_context, bearer_token.as_deref(), repository.clone(), record_key.clone()).await { 396 + match tools::get_event( 397 + web_context, 398 + bearer_token.as_deref(), 399 + repository.clone(), 400 + record_key.clone(), 401 + ) 402 + .await 403 + { 403 404 Ok(text) => { 404 405 tracing::info!( 405 406 repository = %repository, ··· 456 457 "Calling search_events tool" 457 458 ); 458 459 459 - match tools::search_events(web_context, bearer_token.as_deref(), repository.clone(), query.clone()).await { 460 + match tools::search_events( 461 + web_context, 462 + bearer_token.as_deref(), 463 + repository.clone(), 464 + query.clone(), 465 + ) 466 + .await 467 + { 460 468 Ok(text) => { 461 469 tracing::info!( 462 470 repository = ?repository, ··· 539 547 } 540 548 }; 541 549 542 - match tools::create_rsvp(web_context, &bearer_token, repository.clone(), record_key.clone(), status.clone()).await { 550 + match tools::create_rsvp( 551 + web_context, 552 + &bearer_token, 553 + repository.clone(), 554 + record_key.clone(), 555 + status.clone(), 556 + ) 557 + .await 558 + { 543 559 Ok(text) => { 544 560 tracing::info!( 545 561 repository = %repository, ··· 608 624 } 609 625 610 626 /// Internal DELETE handler (no authentication check) 611 - async fn delete_mcp( 612 - State(web_context): State<WebContext>, 613 - headers: HeaderMap, 614 - ) -> Response { 627 + async fn delete_mcp(State(web_context): State<WebContext>, headers: HeaderMap) -> Response { 615 628 // Get session ID from header 616 629 let session_id = headers 617 630 .get(MCP_SESSION_ID_HEADER)
+32 -12
src/http/import_utils.rs
··· 216 216 aturi: &str, 217 217 ) -> Result<()> { 218 218 // Parse the AT-URI 219 - let parsed = ATURI::from_str(aturi) 220 - .with_context(|| format!("Invalid AT-URI: {}", aturi))?; 219 + let parsed = ATURI::from_str(aturi).with_context(|| format!("Invalid AT-URI: {}", aturi))?; 221 220 222 221 let did = &parsed.authority; 223 222 let collection = &parsed.collection; ··· 264 263 // Process the record based on its collection 265 264 match collection.as_str() { 266 265 COMMUNITY_EVENT_NSID => { 267 - handle_event_import(http_client, pool, content_storage, did, rkey, &cid, &record, pds_endpoint).await?; 266 + handle_event_import( 267 + http_client, 268 + pool, 269 + content_storage, 270 + did, 271 + rkey, 272 + &cid, 273 + &record, 274 + pds_endpoint, 275 + ) 276 + .await?; 268 277 } 269 278 COMMUNITY_RSVP_NSID => { 270 279 handle_rsvp_import(pool, record_resolver, did, rkey, &cid, &record).await?; 271 280 } 272 281 PROFILE_NSID => { 273 - handle_profile_import(http_client, pool, content_storage, &document, did, rkey, &cid, &record, pds_endpoint).await?; 282 + handle_profile_import( 283 + http_client, 284 + pool, 285 + content_storage, 286 + &document, 287 + did, 288 + rkey, 289 + &cid, 290 + &record, 291 + pds_endpoint, 292 + ) 293 + .await?; 274 294 } 275 295 ACCEPTANCE_NSID => { 276 296 handle_acceptance_import(pool, did, rkey, &cid, &record).await?; ··· 334 354 let all_media = event_record.media; 335 355 336 356 for media in &all_media { 337 - if let Err(err) = download_media(http_client, content_storage, pds_endpoint, did, media).await { 357 + if let Err(err) = 358 + download_media(http_client, content_storage, pds_endpoint, did, media).await 359 + { 338 360 tracing::error!(error = ?err, "failed processing image"); 339 361 } 340 362 } ··· 468 490 469 491 // Download avatar and banner blobs if present 470 492 if let Some(ref avatar) = profile_record.avatar 471 - && let Err(e) = download_avatar(http_client, content_storage, pds_endpoint, did, avatar).await 493 + && let Err(e) = 494 + download_avatar(http_client, content_storage, pds_endpoint, did, avatar).await 472 495 { 473 496 tracing::warn!( 474 497 error = ?e, ··· 478 501 } 479 502 480 503 if let Some(ref banner) = profile_record.banner 481 - && let Err(e) = download_banner(http_client, content_storage, pds_endpoint, did, banner).await 504 + && let Err(e) = 505 + download_banner(http_client, content_storage, pds_endpoint, did, banner).await 482 506 { 483 507 tracing::warn!( 484 508 error = ?e, ··· 742 766 let image_path = format!("{}.png", blob_ref); 743 767 tracing::info!(?image_path, "image_path"); 744 768 745 - if content_storage 746 - .as_ref() 747 - .content_exists(&image_path) 748 - .await? 749 - { 769 + if content_storage.as_ref().content_exists(&image_path).await? { 750 770 tracing::info!(?image_path, "content exists"); 751 771 return Ok(()); 752 772 }
+6 -5
src/http/mod.rs
··· 7 7 pub mod event_form; 8 8 pub mod event_validation; 9 9 pub mod event_view; 10 + pub mod h3_utils; 10 11 pub mod handle_accept_rsvp; 11 12 pub mod handle_admin_content_list; 12 13 pub mod handle_admin_content_view; 13 14 pub mod handle_admin_denylist; 14 15 pub mod handle_admin_event; 16 + pub mod handle_admin_event_index; 15 17 pub mod handle_admin_events; 16 18 pub mod handle_admin_handles; 17 19 pub mod handle_admin_identity_profile; 18 20 pub mod handle_admin_import_event; 19 21 pub mod handle_admin_import_rsvp; 20 22 pub mod handle_admin_index; 23 + pub mod handle_admin_profile_index; 21 24 pub mod handle_admin_rsvp; 22 25 pub mod handle_admin_rsvp_accept; 23 26 pub mod handle_admin_rsvp_accepts; 24 27 pub mod handle_admin_rsvps; 25 28 pub mod handle_admin_search_index; 26 - pub mod handle_admin_event_index; 27 - pub mod handle_admin_profile_index; 28 29 pub mod handle_blob; 29 30 pub mod handle_bulk_accept_rsvps; 30 31 pub mod handle_content; ··· 39 40 pub mod handle_finalize_acceptance; 40 41 pub mod handle_geo_aggregation; 41 42 pub mod handle_health; 42 - pub mod handle_lfg; 43 - pub mod h3_utils; 44 43 pub mod handle_host_meta; 45 44 pub mod handle_import; 46 45 pub mod handle_index; 46 + pub mod handle_lfg; 47 47 pub mod handle_location; 48 48 pub mod handle_location_suggestions; 49 49 pub mod handle_mailgun_webhook; ··· 58 58 pub mod handle_preview_description; 59 59 pub mod handle_profile; 60 60 pub mod handle_quick_event; 61 - pub mod profile_import; 62 61 pub mod handle_search; 63 62 pub mod handle_set_language; 64 63 pub mod handle_settings; ··· 67 66 pub mod handle_view_event; 68 67 pub mod handle_wellknown; 69 68 pub mod handle_xrpc_get_event; 69 + pub mod handle_xrpc_get_rsvp; 70 70 pub mod handle_xrpc_link_attestation; 71 71 pub mod handle_xrpc_search_events; 72 72 pub mod handler_mcp; ··· 77 77 pub mod middleware_auth; 78 78 pub mod middleware_i18n; 79 79 pub mod pagination; 80 + pub mod profile_import; 80 81 pub mod rsvp_form; 81 82 pub mod server; 82 83 pub mod tab_selector;
+36 -10
src/http/profile_import.rs
··· 18 18 use crate::{ 19 19 atproto::lexicon::{ 20 20 bluesky_profile::{BlueskyProfile, NSID as BLUESKY_PROFILE_NSID}, 21 - profile::{Profile as SmokesignalProfile, NSID as SMOKESIGNAL_PROFILE_NSID}, 21 + profile::{NSID as SMOKESIGNAL_PROFILE_NSID, Profile as SmokesignalProfile}, 22 22 }, 23 23 facets::{FacetLimits, parse_facets_from_text}, 24 - storage::{StoragePool, content::ContentStorage, profile::{profile_get_by_did, profile_insert}}, 24 + storage::{ 25 + StoragePool, 26 + content::ContentStorage, 27 + profile::{profile_get_by_did, profile_insert}, 28 + }, 25 29 }; 26 30 27 31 use super::errors::ProfileImportError; ··· 133 137 }; 134 138 135 139 // Parse facets from description 136 - if let Some(facets) = parse_facets_from_text(&truncated, identity_resolver.as_ref(), facet_limits).await { 140 + if let Some(facets) = 141 + parse_facets_from_text(&truncated, identity_resolver.as_ref(), facet_limits).await 142 + { 137 143 smokesignal_profile.facets = Some(facets); 138 144 } 139 145 ··· 183 189 swap_commit: None, 184 190 }; 185 191 186 - let put_response = put_record(http_client, &Auth::DPoP(dpop_auth.clone()), pds_endpoint, put_request) 187 - .await 188 - .map_err(|e| ProfileImportError::PdsWriteFailed(e.to_string()))?; 192 + let put_response = put_record( 193 + http_client, 194 + &Auth::DPoP(dpop_auth.clone()), 195 + pds_endpoint, 196 + put_request, 197 + ) 198 + .await 199 + .map_err(|e| ProfileImportError::PdsWriteFailed(e.to_string()))?; 189 200 190 201 match put_response { 191 202 PutRecordResponse::StrongRef { uri, cid, .. } => { ··· 198 209 .unwrap_or(handle); 199 210 200 211 // Store profile locally 201 - if let Err(e) = profile_insert(pool, &uri, &cid, did, display_name_for_db, &smokesignal_profile).await { 212 + if let Err(e) = profile_insert( 213 + pool, 214 + &uri, 215 + &cid, 216 + did, 217 + display_name_for_db, 218 + &smokesignal_profile, 219 + ) 220 + .await 221 + { 202 222 tracing::error!(did = %did, error = %e, "Failed to store imported profile locally"); 203 223 return Err(ProfileImportError::StorageFailed(e.to_string())); 204 224 } ··· 243 263 .map_err(|e| ProfileImportError::AvatarProcessFailed(e.to_string()))?; 244 264 245 265 // Upload processed avatar to user's PDS 246 - let new_blob = upload_blob_to_pds(http_client, dpop_auth, pds_endpoint, &processed, "image/png") 247 - .await 248 - .map_err(|e| ProfileImportError::AvatarUploadFailed(e.to_string()))?; 266 + let new_blob = upload_blob_to_pds( 267 + http_client, 268 + dpop_auth, 269 + pds_endpoint, 270 + &processed, 271 + "image/png", 272 + ) 273 + .await 274 + .map_err(|e| ProfileImportError::AvatarUploadFailed(e.to_string()))?; 249 275 250 276 // Store avatar locally in content storage 251 277 let image_path = format!("{}.png", new_blob.inner.ref_.link);
+39 -20
src/http/server.rs
··· 24 24 handle_admin_denylist, handle_admin_denylist_add, handle_admin_denylist_remove, 25 25 }, 26 26 handle_admin_event::{handle_admin_event, handle_admin_event_nuke}, 27 + handle_admin_event_index::{ 28 + handle_admin_event_index, handle_admin_event_index_delete, handle_admin_event_index_rebuild, 29 + }, 27 30 handle_admin_events::handle_admin_events, 28 - handle_admin_handles::{handle_admin_handles, handle_admin_nuke_identity, handle_admin_tap_sync_all}, 31 + handle_admin_handles::{ 32 + handle_admin_handles, handle_admin_nuke_identity, handle_admin_tap_sync_all, 33 + }, 29 34 handle_admin_identity_profile::{ 30 35 handle_admin_identity_profile, handle_admin_identity_profile_ban_did, 31 36 handle_admin_identity_profile_ban_pds, handle_admin_identity_profile_nuke, ··· 33 38 handle_admin_import_event::handle_admin_import_event, 34 39 handle_admin_import_rsvp::handle_admin_import_rsvp, 35 40 handle_admin_index::handle_admin_index, 41 + handle_admin_profile_index::{ 42 + handle_admin_profile_index, handle_admin_profile_index_delete, 43 + handle_admin_profile_index_rebuild, 44 + }, 36 45 handle_admin_rsvp::{handle_admin_rsvp, handle_admin_rsvp_nuke}, 37 46 handle_admin_rsvp_accept::{handle_admin_rsvp_accept, handle_admin_rsvp_accept_nuke}, 38 47 handle_admin_rsvp_accepts::handle_admin_rsvp_accepts, ··· 41 50 handle_admin_search_index, handle_admin_search_index_delete, 42 51 handle_admin_search_index_rebuild, 43 52 }, 44 - handle_admin_event_index::{ 45 - handle_admin_event_index, handle_admin_event_index_delete, 46 - handle_admin_event_index_rebuild, 47 - }, 48 - handle_admin_profile_index::{ 49 - handle_admin_profile_index, handle_admin_profile_index_delete, 50 - handle_admin_profile_index_rebuild, 51 - }, 52 53 handle_blob::{ 53 54 delete_profile_avatar, delete_profile_banner, upload_event_header, upload_event_thumbnail, 54 55 upload_profile_avatar, upload_profile_banner, ··· 66 67 handle_finalize_acceptance::handle_finalize_acceptance, 67 68 handle_geo_aggregation::{handle_geo_aggregation, handle_globe_aggregation}, 68 69 handle_health::{handle_alive, handle_ready, handle_started}, 70 + handle_host_meta::handle_host_meta, 71 + handle_import::{handle_import, handle_import_submit}, 72 + handle_index::handle_index, 69 73 handle_lfg::{ 70 74 handle_lfg_deactivate, handle_lfg_geo_aggregation, handle_lfg_get, handle_lfg_post, 71 75 handle_lfg_tags_autocomplete, 72 76 }, 73 - handle_host_meta::handle_host_meta, 74 - handle_import::{handle_import, handle_import_submit}, 75 - handle_index::handle_index, 76 77 handle_location::handle_location, 77 78 handle_location_suggestions::handle_location_suggestions, 78 79 handle_mailgun_webhook::handle_mailgun_webhook, ··· 89 90 handle_search::handle_search, 90 91 handle_set_language::handle_set_language, 91 92 handle_settings::{ 92 - handle_email_update, handle_language_update, 93 - handle_notification_email_update, handle_notification_preferences_update, 94 - handle_profile_update, handle_settings, 93 + handle_email_update, handle_language_update, handle_notification_email_update, 94 + handle_notification_preferences_update, handle_profile_update, handle_settings, 95 95 handle_timezone_update, 96 96 }, 97 97 handle_unaccept_rsvp::handle_unaccept_rsvp, ··· 99 99 handle_view_event::handle_view_event, 100 100 handle_wellknown::handle_wellknown_did_web, 101 101 handle_xrpc_get_event::handle_xrpc_get_event, 102 + handle_xrpc_get_rsvp::handle_xrpc_get_rsvp, 102 103 handle_xrpc_link_attestation::handle_xrpc_link_attestation, 103 104 handle_xrpc_search_events::handle_xrpc_search_events, 104 105 handler_mcp::{ ··· 151 152 "/xrpc/community.lexicon.calendar.searchEvents", 152 153 get(handle_xrpc_search_events), 153 154 ) 155 + // legacy endpoint 154 156 .route( 155 157 "/xrpc/community.lexicon.calendar.SearchEvents", 156 158 get(handle_xrpc_search_events), ··· 159 161 "/xrpc/community.lexicon.calendar.getEvent", 160 162 get(handle_xrpc_get_event), 161 163 ) 162 - .route( 164 + // legacy endpoint 165 + .route( 163 166 "/xrpc/community.lexicon.calendar.GetEvent", 164 167 get(handle_xrpc_get_event), 165 168 ) 166 169 .route( 170 + "/xrpc/community.lexicon.calendar.getRSVP", 171 + get(handle_xrpc_get_rsvp), 172 + ) 173 + .route( 167 174 "/xrpc/events.smokesignal.rsvp.linkAttestation", 168 175 post(handle_xrpc_link_attestation), 169 176 ) ··· 268 275 "/admin/search-index/events/rebuild", 269 276 post(handle_admin_event_index_rebuild), 270 277 ) 271 - .route("/admin/search-index/profiles", get(handle_admin_profile_index)) 278 + .route( 279 + "/admin/search-index/profiles", 280 + get(handle_admin_profile_index), 281 + ) 272 282 .route( 273 283 "/admin/search-index/profiles/delete", 274 284 post(handle_admin_profile_index_delete), ··· 319 329 .route("/quick-event", get(handle_quick_event)) 320 330 .route("/event", get(handle_create_event)) 321 331 .route("/event", post(handle_create_event_json)) 322 - .route("/event/preview-description", post(handle_preview_description)) 323 - .route("/event/location-suggestions", get(handle_location_suggestions)) 332 + .route( 333 + "/event/preview-description", 334 + post(handle_preview_description), 335 + ) 336 + .route( 337 + "/event/location-suggestions", 338 + get(handle_location_suggestions), 339 + ) 324 340 .route("/rsvp", get(handle_create_rsvp)) 325 341 .route("/rsvp", post(handle_create_rsvp)) 326 342 .route("/accept_rsvp", post(handle_accept_rsvp)) ··· 337 353 ) 338 354 .route("/ics/{*aturi}", get(handle_export_ics)) 339 355 .route("/{handle_slug}/ics", get(handle_event_ics)) 340 - .route("/{handle_slug}/{event_rkey}/edit", post(handle_edit_event_json)) 356 + .route( 357 + "/{handle_slug}/{event_rkey}/edit", 358 + post(handle_edit_event_json), 359 + ) 341 360 .route( 342 361 "/{handle_slug}/{event_rkey}/manage", 343 362 get(handle_manage_event),
+5 -6
src/image.rs
··· 13 13 } 14 14 15 15 // Try to load the image to ensure it's valid 16 - image::load_from_memory(data) 17 - .map_err(|e| ImageError::InvalidImageData(e.to_string()))?; 16 + image::load_from_memory(data).map_err(|e| ImageError::InvalidImageData(e.to_string()))?; 18 17 19 18 Ok(()) 20 19 } ··· 22 21 /// Process avatar image: validate 1:1 aspect ratio, resize to 400x400, convert to PNG 23 22 pub(crate) fn process_avatar(data: &[u8]) -> Result<Vec<u8>, ImageError> { 24 23 // Load the image 25 - let img = image::load_from_memory(data) 26 - .map_err(|e| ImageError::AvatarLoadFailed(e.to_string()))?; 24 + let img = 25 + image::load_from_memory(data).map_err(|e| ImageError::AvatarLoadFailed(e.to_string()))?; 27 26 28 27 let (width, height) = img.dimensions(); 29 28 ··· 52 51 /// Process banner image: validate 16:9 aspect ratio, resize to 1600x900, convert to PNG 53 52 pub(crate) fn process_banner(data: &[u8]) -> Result<Vec<u8>, ImageError> { 54 53 // Load the image 55 - let img = image::load_from_memory(data) 56 - .map_err(|e| ImageError::BannerLoadFailed(e.to_string()))?; 54 + let img = 55 + image::load_from_memory(data).map_err(|e| ImageError::BannerLoadFailed(e.to_string()))?; 57 56 58 57 let (width, height) = img.dimensions(); 59 58
+6 -2
src/image_errors.rs
··· 88 88 /// 89 89 /// This error occurs when an event header image doesn't have the required 90 90 /// 3:1 aspect ratio (allowing 10% deviation). 91 - #[error("error-smokesignal-image-10 Event header must have 3:1 aspect ratio: got {width}:{height}")] 91 + #[error( 92 + "error-smokesignal-image-10 Event header must have 3:1 aspect ratio: got {width}:{height}" 93 + )] 92 94 InvalidEventHeaderAspectRatio { 93 95 /// The width of the image in pixels. 94 96 width: u32, ··· 114 116 /// 115 117 /// This error occurs when a thumbnail image doesn't have the required 116 118 /// square aspect ratio (allowing 5% deviation). 117 - #[error("error-smokesignal-image-13 Thumbnail must have 1:1 aspect ratio: got {width}:{height}")] 119 + #[error( 120 + "error-smokesignal-image-13 Thumbnail must have 1:1 aspect ratio: got {width}:{height}" 121 + )] 118 122 InvalidThumbnailAspectRatio { 119 123 /// The width of the image in pixels. 120 124 width: u32,
+2 -2
src/lib.rs
··· 18 18 pub mod mcp; 19 19 pub mod processor; 20 20 pub mod processor_errors; 21 + pub mod profile_index; 22 + pub mod profile_index_errors; 21 23 pub mod record_resolver; 22 24 pub mod refresh_tokens_errors; 23 25 pub mod search_index; 24 - pub mod profile_index; 25 - pub mod profile_index_errors; 26 26 pub mod service; 27 27 pub mod stats; 28 28 pub mod storage;
+62 -63
src/mcp/tools.rs
··· 1 1 use anyhow::Result; 2 - use atproto_client::com::atproto::repo::{put_record, PutRecordRequest, PutRecordResponse}; 3 - use atproto_identity::resolve::{parse_input, InputType}; 2 + use atproto_client::com::atproto::repo::{PutRecordRequest, PutRecordResponse, put_record}; 3 + use atproto_identity::resolve::{InputType, parse_input}; 4 4 use atproto_record::lexicon::community::lexicon::calendar::event::NSID as CommunityLexiconCalendarEventNSID; 5 - use atproto_record::lexicon::community::lexicon::calendar::rsvp::{Rsvp, RsvpStatus, NSID as RsvpNSID}; 5 + use atproto_record::lexicon::community::lexicon::calendar::rsvp::{ 6 + NSID as RsvpNSID, Rsvp, RsvpStatus, 7 + }; 6 8 use chrono::Utc; 7 9 use metrohash::MetroHash64; 8 10 use std::hash::Hasher; ··· 11 13 use crate::config::OAuthBackendConfig; 12 14 use crate::http::{context::WebContext, utils::url_from_aturi}; 13 15 use crate::search_index::SearchIndexManager; 14 - use crate::storage::event::{event_get, get_event_rsvp_counts, rsvp_get_by_event_and_did, rsvp_insert_with_metadata, RsvpInsertParams}; 16 + use crate::storage::event::{ 17 + RsvpInsertParams, event_get, get_event_rsvp_counts, rsvp_get_by_event_and_did, 18 + rsvp_insert_with_metadata, 19 + }; 15 20 16 21 /// Wrap response text with safety tags to prevent prompt injection 17 22 pub fn wrap_response_text(text: String) -> String { ··· 44 49 } 45 50 Ok(InputType::Handle(_)) | Err(_) => { 46 51 tracing::warn!(repository = %repository, "Invalid repository: not a DID"); 47 - return Err("Invalid repository: must be a DID (did:plc:... or did:web:...)".to_string()); 52 + return Err( 53 + "Invalid repository: must be a DID (did:plc:... or did:web:...)".to_string(), 54 + ); 48 55 } 49 56 }; 50 57 ··· 58 65 59 66 // Fetch event 60 67 tracing::debug!(aturi = %aturi, "Fetching event from database"); 61 - let event = event_get(&web_context.pool, &aturi) 62 - .await 63 - .map_err(|e| { 64 - tracing::error!(aturi = %aturi, error = ?e, "Failed to fetch event from database"); 65 - "Event not found".to_string() 66 - })?; 68 + let event = event_get(&web_context.pool, &aturi).await.map_err(|e| { 69 + tracing::error!(aturi = %aturi, error = ?e, "Failed to fetch event from database"); 70 + "Event not found".to_string() 71 + })?; 67 72 68 73 tracing::debug!(aturi = %aturi, "Event fetched successfully"); 69 74 ··· 94 99 match cache.validate_token(token).await { 95 100 Ok(Some(user_info)) => { 96 101 tracing::debug!(did = %user_info.sub, "Checking caller's RSVP status"); 97 - match rsvp_get_by_event_and_did(&web_context.pool, &aturi, &user_info.sub).await { 102 + match rsvp_get_by_event_and_did(&web_context.pool, &aturi, &user_info.sub).await 103 + { 98 104 Ok(Some(rsvp)) => { 99 105 // Parse the RSVP record to get the status 100 - let status_str = rsvp.record.0.get("status") 106 + let status_str = rsvp 107 + .record 108 + .0 109 + .get("status") 101 110 .and_then(|v| v.as_str()) 102 111 .unwrap_or("unknown"); 103 112 tracing::debug!(status = %status_str, "Found caller's RSVP"); ··· 123 132 124 133 // Generate event URL 125 134 tracing::debug!(aturi = %aturi, "Generating event URL"); 126 - let url = url_from_aturi(&web_context.config.external_base, &event.aturi) 127 - .map_err(|e| { 128 - tracing::error!(aturi = %aturi, error = ?e, "Failed to generate URL"); 129 - format!("Error generating URL: {}", e) 130 - })?; 135 + let url = url_from_aturi(&web_context.config.external_base, &event.aturi).map_err(|e| { 136 + tracing::error!(aturi = %aturi, error = ?e, "Failed to generate URL"); 137 + format!("Error generating URL: {}", e) 138 + })?; 131 139 132 140 // Extract event details from record 133 141 let record = &event.record.0; ··· 295 303 "Fetching event details" 296 304 ); 297 305 298 - let event = event_get(&web_context.pool, aturi) 299 - .await 300 - .map_err(|e| { 301 - tracing::error!(aturi = %aturi, error = ?e, "Error fetching event"); 302 - format!("Error fetching event {}: {}", aturi, e) 303 - })?; 306 + let event = event_get(&web_context.pool, aturi).await.map_err(|e| { 307 + tracing::error!(aturi = %aturi, error = ?e, "Error fetching event"); 308 + format!("Error fetching event {}: {}", aturi, e) 309 + })?; 304 310 305 311 let rsvp_counts = get_event_rsvp_counts(&web_context.pool, vec![aturi.clone()]) 306 312 .await ··· 363 369 match rsvp_get_by_event_and_did(&web_context.pool, aturi, did).await { 364 370 Ok(Some(rsvp)) => { 365 371 // Parse the RSVP record to get the status 366 - let status_str = rsvp.record.0.get("status") 372 + let status_str = rsvp 373 + .record 374 + .0 375 + .get("status") 367 376 .and_then(|v| v.as_str()) 368 377 .unwrap_or("unknown"); 369 378 Some(status_str.to_string()) 370 379 } 371 - _ => None 380 + _ => None, 372 381 } 373 382 } else { 374 383 None ··· 428 437 429 438 // Validate bearer token to get user info 430 439 let user_info = match &web_context.aip_auth_cache { 431 - Some(cache) => { 432 - cache.validate_token(bearer_token) 433 - .await 434 - .map_err(|e| { 435 - tracing::error!(error = ?e, "Failed to validate bearer token"); 436 - "Authentication failed".to_string() 437 - })? 438 - .ok_or_else(|| { 439 - tracing::warn!("Invalid bearer token"); 440 - "Invalid authentication token".to_string() 441 - })? 442 - } 440 + Some(cache) => cache 441 + .validate_token(bearer_token) 442 + .await 443 + .map_err(|e| { 444 + tracing::error!(error = ?e, "Failed to validate bearer token"); 445 + "Authentication failed".to_string() 446 + })? 447 + .ok_or_else(|| { 448 + tracing::warn!("Invalid bearer token"); 449 + "Invalid authentication token".to_string() 450 + })?, 443 451 None => { 444 452 tracing::error!("AIP auth cache not configured"); 445 453 return Err("Authentication not available".to_string()); ··· 457 465 } 458 466 Ok(InputType::Handle(_)) | Err(_) => { 459 467 tracing::warn!(repository = %repository, "Invalid repository: not a DID"); 460 - return Err("Invalid repository: must be a DID (did:plc:... or did:web:...)".to_string()); 468 + return Err( 469 + "Invalid repository: must be a DID (did:plc:... or did:web:...)".to_string(), 470 + ); 461 471 } 462 472 }; 463 473 ··· 501 511 // Create DPoP auth from AIP session 502 512 let dpop_auth = match &web_context.config.oauth_backend { 503 513 OAuthBackendConfig::AIP { hostname, .. } => { 504 - create_dpop_auth_from_aip_session( 505 - &web_context.http_client, 506 - hostname, 507 - bearer_token, 508 - ) 509 - .await 510 - .map_err(|e| { 511 - tracing::error!(error = ?e, "Failed to create DPoP auth"); 512 - format!("Authentication error: {}", e) 513 - })? 514 + create_dpop_auth_from_aip_session(&web_context.http_client, hostname, bearer_token) 515 + .await 516 + .map_err(|e| { 517 + tracing::error!(error = ?e, "Failed to create DPoP auth"); 518 + format!("Authentication error: {}", e) 519 + })? 514 520 } 515 521 _ => { 516 522 tracing::error!("OAuth backend is not AIP"); ··· 539 545 tracing::debug!(record_key = %rsvp_record_key, "Generated RSVP record key"); 540 546 541 547 // Check for existing RSVP 542 - let existing_rsvp = rsvp_get_by_event_and_did( 543 - &web_context.pool, 544 - &event_aturi, 545 - user_did, 546 - ) 547 - .await 548 - .ok() 549 - .flatten(); 548 + let existing_rsvp = rsvp_get_by_event_and_did(&web_context.pool, &event_aturi, user_did) 549 + .await 550 + .ok() 551 + .flatten(); 550 552 551 553 let now = Utc::now(); 552 554 ··· 690 692 ); 691 693 692 694 // Generate event URL 693 - let event_url = url_from_aturi(&web_context.config.external_base, &event_aturi) 694 - .map_err(|e| { 695 + let event_url = 696 + url_from_aturi(&web_context.config.external_base, &event_aturi).map_err(|e| { 695 697 tracing::error!(error = ?e, "Failed to generate event URL"); 696 698 format!("Error generating URL: {}", e) 697 699 })?; ··· 699 701 // Format response 700 702 let text = format!( 701 703 "RSVP created successfully\nEvent: {}\nEvent URL: {}\nStatus: {}\nRSVP AT-URI: {}", 702 - event.name, 703 - event_url, 704 - status, 705 - create_record_result.uri 704 + event.name, event_url, status, create_record_result.uri 706 705 ); 707 706 708 707 Ok(wrap_response_text(text))
+3 -6
src/processor.rs
··· 43 43 use crate::storage::acceptance::{ 44 44 acceptance_record_delete, acceptance_record_upsert, rsvp_update_validated_at, 45 45 }; 46 + use crate::storage::atproto_record::{atproto_record_delete, atproto_record_upsert}; 46 47 use crate::storage::content::ContentStorage; 47 48 use crate::storage::denylist::denylist_exists; 48 49 use crate::storage::event::EventInsertParams; ··· 52 53 use crate::storage::event::event_insert_with_metadata; 53 54 use crate::storage::event::rsvp_delete; 54 55 use crate::storage::event::rsvp_insert_with_metadata; 55 - use crate::storage::atproto_record::{atproto_record_delete, atproto_record_upsert}; 56 56 use crate::storage::profile::profile_delete; 57 57 use crate::storage::profile::profile_insert; 58 58 use atproto_record::lexicon::community::lexicon::calendar::event::{ ··· 125 125 ) -> Result<()> { 126 126 match collection { 127 127 "community.lexicon.calendar.event" => { 128 - self.handle_event_commit(did, rkey, cid, record, live) 129 - .await 128 + self.handle_event_commit(did, rkey, cid, record, live).await 130 129 } 131 130 "community.lexicon.calendar.rsvp" => { 132 131 self.handle_rsvp_commit(did, rkey, cid, record, live).await ··· 156 155 live: bool, 157 156 ) -> Result<()> { 158 157 match collection { 159 - "community.lexicon.calendar.event" => { 160 - self.handle_event_delete(did, rkey, live).await 161 - } 158 + "community.lexicon.calendar.event" => self.handle_event_delete(did, rkey, live).await, 162 159 "community.lexicon.calendar.rsvp" => self.handle_rsvp_delete(did, rkey, live).await, 163 160 "events.smokesignal.profile" => self.handle_profile_delete(did, rkey).await, 164 161 "events.smokesignal.calendar.acceptance" => {
+9 -3
src/profile_index.rs
··· 12 12 use crate::atproto::lexicon::profile::Profile as ProfileRecord; 13 13 use crate::atproto::utils::get_profile_hashtags; 14 14 use crate::profile_index_errors::ProfileIndexError; 15 - use crate::storage::{identity_profile::handle_for_did, profile::profile_list, StoragePool}; 15 + use crate::storage::{StoragePool, identity_profile::handle_for_did, profile::profile_list}; 16 16 17 17 const INDEX_NAME: &str = "smokesignal-profiles"; 18 18 ··· 369 369 } 370 370 371 371 for profile in &profiles { 372 - match self.index_profile(pool, identity_resolver.clone(), profile).await { 372 + match self 373 + .index_profile(pool, identity_resolver.clone(), profile) 374 + .await 375 + { 373 376 Ok(_) => { 374 377 indexed_count += 1; 375 378 tracing::debug!("Indexed profile {}", profile.aturi); ··· 397 400 } 398 401 } 399 402 400 - tracing::info!("Profile index rebuild complete: {} profiles indexed", indexed_count); 403 + tracing::info!( 404 + "Profile index rebuild complete: {} profiles indexed", 405 + indexed_count 406 + ); 401 407 Ok(indexed_count) 402 408 } 403 409 }
+31 -12
src/search_index.rs
··· 11 11 use std::sync::Arc; 12 12 13 13 use crate::atproto::utils::get_event_hashtags; 14 - use crate::storage::{event::event_list, identity_profile::handle_for_did, StoragePool}; 14 + use crate::storage::{StoragePool, event::event_list, identity_profile::handle_for_did}; 15 15 use atproto_record::lexicon::community::lexicon::calendar::event::Event; 16 16 use atproto_record::lexicon::community::lexicon::location::LocationOrRef; 17 17 ··· 463 463 } 464 464 465 465 for event in &events { 466 - match self.index_event(pool, identity_resolver.clone(), event).await { 466 + match self 467 + .index_event(pool, identity_resolver.clone(), event) 468 + .await 469 + { 467 470 Ok(_) => { 468 471 indexed_count += 1; 469 472 tracing::debug!("Indexed event {}", event.aturi); ··· 1330 1333 1331 1334 if !response.status_code().is_success() { 1332 1335 let error_body = response.text().await?; 1333 - return Err(anyhow::anyhow!("Failed to create LFG index: {}", error_body)); 1336 + return Err(anyhow::anyhow!( 1337 + "Failed to create LFG index: {}", 1338 + error_body 1339 + )); 1334 1340 } 1335 1341 1336 1342 tracing::info!("Created OpenSearch index {}", Self::LFG_INDEX_NAME); ··· 1562 1568 1563 1569 let response = self 1564 1570 .client 1565 - .update_by_query(opensearch::UpdateByQueryParts::Index(&[Self::LFG_INDEX_NAME])) 1571 + .update_by_query(opensearch::UpdateByQueryParts::Index(&[ 1572 + Self::LFG_INDEX_NAME, 1573 + ])) 1566 1574 .body(update_body) 1567 1575 .send() 1568 1576 .await?; 1569 1577 1570 1578 if !response.status_code().is_success() { 1571 1579 let error_body = response.text().await?; 1572 - return Err(anyhow::anyhow!("Failed to deactivate expired LFG profiles: {}", error_body)); 1580 + return Err(anyhow::anyhow!( 1581 + "Failed to deactivate expired LFG profiles: {}", 1582 + error_body 1583 + )); 1573 1584 } 1574 1585 1575 1586 let body = response.json::<Value>().await?; ··· 1618 1629 LocationOrRef::InlineGeo(_) => "InlineGeo", 1619 1630 _ => "Other", 1620 1631 }; 1621 - assert_eq!(variant0, "InlineAddress", "First location should be InlineAddress"); 1632 + assert_eq!( 1633 + variant0, "InlineAddress", 1634 + "First location should be InlineAddress" 1635 + ); 1622 1636 1623 1637 // Check second location is a geo 1624 1638 let variant1 = match &locations[1] { ··· 1633 1647 assert_eq!(variant1, "InlineGeo", "Second location should be InlineGeo"); 1634 1648 1635 1649 // Test extract_geo_point 1636 - let geo_points: Vec<GeoPoint> = locations 1637 - .iter() 1638 - .filter_map(extract_geo_point) 1639 - .collect(); 1650 + let geo_points: Vec<GeoPoint> = locations.iter().filter_map(extract_geo_point).collect(); 1640 1651 1641 1652 assert_eq!(geo_points.len(), 1, "Should extract 1 geo point"); 1642 1653 assert!((geo_points[0].lat - 39.707931).abs() < 0.0001); ··· 1671 1682 1672 1683 // Parse back from Value (simulating database retrieval) 1673 1684 let event_roundtrip: EventLexicon = serde_json::from_value(serialized).unwrap(); 1674 - assert_eq!(event_roundtrip.locations.len(), 2, "Should still have 2 locations after roundtrip"); 1685 + assert_eq!( 1686 + event_roundtrip.locations.len(), 1687 + 2, 1688 + "Should still have 2 locations after roundtrip" 1689 + ); 1675 1690 1676 1691 // Check that geo location is still correctly identified 1677 1692 let geo_points: Vec<GeoPoint> = event_roundtrip ··· 1680 1695 .filter_map(extract_geo_point) 1681 1696 .collect(); 1682 1697 1683 - assert_eq!(geo_points.len(), 1, "Should extract 1 geo point after roundtrip"); 1698 + assert_eq!( 1699 + geo_points.len(), 1700 + 1, 1701 + "Should extract 1 geo point after roundtrip" 1702 + ); 1684 1703 assert!((geo_points[0].lat - 39.707931).abs() < 0.0001); 1685 1704 assert!((geo_points[0].lon - (-84.1648089)).abs() < 0.0001); 1686 1705 }
+3 -6
src/stats.rs
··· 142 142 } 143 143 144 144 /// Set stats in Redis cache with TTL 145 - async fn set_cached_stats( 146 - cache_pool: &CachePool, 147 - stats: &NetworkStats, 148 - ) -> Result<(), StatsError> { 149 - let json = serde_json::to_string(stats) 150 - .map_err(|e| StatsError::SerializationError(e.to_string()))?; 145 + async fn set_cached_stats(cache_pool: &CachePool, stats: &NetworkStats) -> Result<(), StatsError> { 146 + let json = 147 + serde_json::to_string(stats).map_err(|e| StatsError::SerializationError(e.to_string()))?; 151 148 152 149 let mut conn = cache_pool 153 150 .get()
+16 -10
src/storage/atproto_record.rs
··· 166 166 .map_err(StorageError::UnableToExecuteQuery)?; 167 167 168 168 // Query 2: events with locations created by this user 169 - let event_rows: Vec<(String, Option<DateTime<Utc>>, sqlx::types::Json<serde_json::Value>)> = 170 - sqlx::query_as( 171 - r#" 169 + let event_rows: Vec<( 170 + String, 171 + Option<DateTime<Utc>>, 172 + sqlx::types::Json<serde_json::Value>, 173 + )> = sqlx::query_as( 174 + r#" 172 175 SELECT aturi, updated_at, record 173 176 FROM events 174 177 WHERE did = $1 ··· 176 179 ORDER BY updated_at DESC NULLS LAST 177 180 LIMIT $2 178 181 "#, 179 - ) 180 - .bind(did) 181 - .bind(fetch_limit) 182 - .fetch_all(pool) 183 - .await 184 - .map_err(StorageError::UnableToExecuteQuery)?; 182 + ) 183 + .bind(did) 184 + .bind(fetch_limit) 185 + .fetch_all(pool) 186 + .await 187 + .map_err(StorageError::UnableToExecuteQuery)?; 185 188 186 189 // Collect all suggestions with (priority, timestamp) for sorting 187 190 // Priority: 0 = beaconbits/dropanchor (higher priority), 1 = smokesignal events ··· 262 265 /// Extract locations from an event record's locations array. 263 266 /// 264 267 /// Handles both address and geo location types based on the `$type` field. 265 - fn extract_locations_from_event(aturi: &str, record: &serde_json::Value) -> Vec<LocationSuggestion> { 268 + fn extract_locations_from_event( 269 + aturi: &str, 270 + record: &serde_json::Value, 271 + ) -> Vec<LocationSuggestion> { 266 272 let Some(locations) = record.get("locations").and_then(|l| l.as_array()) else { 267 273 return vec![]; 268 274 };
+1 -1
src/storage/event.rs
··· 81 81 pub rsvp_status: Option<String>, 82 82 pub discovered_at: DateTime<Utc>, // When record was first seen by the system 83 83 pub created_at: Option<DateTime<Utc>>, // From the AT Protocol record metadata 84 - pub h3_cell: Option<String>, // H3 cell for LFG location grouping 84 + pub h3_cell: Option<String>, // H3 cell for LFG location grouping 85 85 } 86 86 } 87 87
+1 -1
src/storage/lfg.rs
··· 9 9 //! let lfg: Lfg = serde_json::from_value(record.record.0.clone())?; 10 10 //! ``` 11 11 12 + use super::StoragePool; 12 13 use super::atproto_record::AtprotoRecord; 13 14 use super::errors::StorageError; 14 - use super::StoragePool; 15 15 use crate::atproto::lexicon::lfg::NSID; 16 16 17 17 /// Get the active LFG record for a DID from atproto_records.
+13 -13
src/tap_processor.rs
··· 4 4 //! eliminating the need for channel-based message passing. 5 5 6 6 use anyhow::Result; 7 - use atproto_tap::{connect, RecordAction, TapConfig, TapEvent}; 7 + use atproto_tap::{RecordAction, TapConfig, TapEvent, connect}; 8 8 use std::sync::Arc; 9 9 use std::time::Duration; 10 10 use tokio_stream::StreamExt; ··· 114 114 let live = record.live; 115 115 116 116 if !live { 117 - tracing::debug!( 118 - "Processing backfill event: {} {} {}", 119 - collection, 120 - did, 121 - rkey 122 - ); 117 + tracing::debug!("Processing backfill event: {} {} {}", collection, did, rkey); 123 118 } 124 119 125 120 match record.action { ··· 139 134 None 140 135 }; 141 136 142 - let content_future = self 143 - .content_fetcher 144 - .handle_commit(did, collection, rkey, cid, record_value, live); 137 + let content_future = self.content_fetcher.handle_commit( 138 + did, 139 + collection, 140 + rkey, 141 + cid, 142 + record_value, 143 + live, 144 + ); 145 145 146 146 let index_future = async { 147 147 if let (Some(indexer), Some(record)) = ··· 165 165 } 166 166 RecordAction::Delete => { 167 167 // Process content fetcher and search indexer delete in parallel 168 - let content_future = 169 - self.content_fetcher 170 - .handle_delete(did, collection, rkey, live); 168 + let content_future = self 169 + .content_fetcher 170 + .handle_delete(did, collection, rkey, live); 171 171 172 172 let index_future = async { 173 173 if let Some(ref indexer) = self.search_indexer {
+6 -6
src/task_lfg_cleanup.rs
··· 8 8 use tokio::time::{Instant, sleep}; 9 9 use tokio_util::sync::CancellationToken; 10 10 11 + use crate::atproto::lexicon::lfg::NSID; 11 12 use crate::search_index::SearchIndexManager; 12 13 use crate::storage::StoragePool; 13 - use crate::atproto::lexicon::lfg::NSID; 14 14 15 15 /// Configuration for the LFG cleanup task. 16 16 pub struct LfgCleanupTaskConfig { ··· 113 113 } 114 114 115 115 /// Deactivate expired LFG records in the database. 116 - async fn deactivate_expired_in_database( 117 - &self, 118 - now: &chrono::DateTime<Utc>, 119 - ) -> Result<u64> { 116 + async fn deactivate_expired_in_database(&self, now: &chrono::DateTime<Utc>) -> Result<u64> { 120 117 // Query for active LFG records that have expired 121 118 let result = sqlx::query( 122 119 r#" ··· 144 141 match search_index.deactivate_expired_lfg_profiles().await { 145 142 Ok(count) => Ok(count), 146 143 Err(err) => { 147 - tracing::warn!("Failed to deactivate expired LFG profiles in OpenSearch: {}", err); 144 + tracing::warn!( 145 + "Failed to deactivate expired LFG profiles in OpenSearch: {}", 146 + err 147 + ); 148 148 Ok(0) 149 149 } 150 150 }
+10 -4
src/task_search_indexer.rs
··· 10 10 use std::sync::Arc; 11 11 12 12 use crate::atproto::lexicon::lfg::{Lfg, NSID as LFG_NSID}; 13 - use crate::atproto::lexicon::profile::{Profile, NSID as PROFILE_NSID}; 13 + use crate::atproto::lexicon::profile::{NSID as PROFILE_NSID, Profile}; 14 14 use crate::atproto::utils::get_profile_hashtags; 15 15 use crate::search_index::SearchIndexManager; 16 - use crate::storage::event::event_get; 17 16 use crate::storage::StoragePool; 17 + use crate::storage::event::event_get; 18 18 use crate::task_search_indexer_errors::SearchIndexerError; 19 19 20 20 /// Build an AT URI with pre-allocated capacity to avoid format! overhead. ··· 285 285 let aturi = build_aturi(did, LexiconCommunityEventNSID, rkey); 286 286 287 287 // Delegate to SearchIndexManager for consistent deletion logic 288 - self.event_index_manager.delete_indexed_event(&aturi).await?; 288 + self.event_index_manager 289 + .delete_indexed_event(&aturi) 290 + .await?; 289 291 290 292 tracing::debug!("Deleted event {} for DID {} from search index", rkey, did); 291 293 Ok(()) ··· 384 386 // Delegate to SearchIndexManager for consistent deletion logic 385 387 self.event_index_manager.delete_lfg_profile(&aturi).await?; 386 388 387 - tracing::debug!("Deleted LFG profile {} for DID {} from search index", rkey, did); 389 + tracing::debug!( 390 + "Deleted LFG profile {} for DID {} from search index", 391 + rkey, 392 + did 393 + ); 388 394 Ok(()) 389 395 } 390 396