The smokesignal.events web application

refactor: improving profile content and blob handling

+175 -7
+150 -5
src/http/handle_blob.rs
··· 25 25 middleware_i18n::Language, 26 26 }, 27 27 select_template, 28 - storage::profile::profile_get_by_aturi, 28 + storage::profile::{profile_get_by_aturi, profile_insert}, 29 29 }; 30 30 31 31 use serde::{Deserialize, Serialize}; ··· 42 42 width: u32, 43 43 height: u32, 44 44 size: usize, 45 + } 46 + 47 + #[derive(Deserialize)] 48 + struct PutRecordSuccessResponse { 49 + uri: String, 50 + cid: String, 45 51 } 46 52 47 53 /// Upload a blob to the PDS and return the TypedBlob reference ··· 138 144 ) 139 145 .await?; 140 146 147 + // Store the avatar image locally in content storage immediately 148 + // This ensures the image is available when the page reloads, without waiting for Jetstream 149 + let avatar_cid = &blob.inner.ref_.link; 150 + let image_path = format!("{}.png", avatar_cid); 151 + if let Err(err) = web_context 152 + .content_storage 153 + .write_content(&image_path, &processed_data) 154 + .await 155 + { 156 + tracing::warn!( 157 + ?err, 158 + cid = %avatar_cid, 159 + "Failed to store avatar image locally, will be fetched via Jetstream" 160 + ); 161 + } 162 + 141 163 // Get the profile aturi 142 164 let profile_aturi = format!( 143 165 "at://{}/events.smokesignal.profile/self", ··· 193 215 let put_record_url = format!("{}/xrpc/com.atproto.repo.putRecord", pds_endpoint); 194 216 let record_value = serde_json::to_value(&put_record_request) 195 217 .map_err(|e| BlobError::PutRecordSerializeFailed(e.to_string()))?; 196 - let _response = match post_dpop_json( 218 + let response = match post_dpop_json( 197 219 &web_context.http_client, 198 220 &dpop_auth, 199 221 &put_record_url, ··· 223 245 } 224 246 }; 225 247 248 + // Update the profile in the local database immediately 249 + // This ensures the profile is visible without waiting for Jetstream 250 + if let Ok(put_response) = serde_json::from_value::<PutRecordSuccessResponse>(response) { 251 + let display_name_for_db = profile 252 + .display_name 253 + .as_ref() 254 + .filter(|s| !s.trim().is_empty()) 255 + .map(|s| s.as_str()) 256 + .unwrap_or(&current_handle.handle); 257 + 258 + if let Err(err) = profile_insert( 259 + &web_context.pool, 260 + &put_response.uri, 261 + &put_response.cid, 262 + &current_handle.did, 263 + display_name_for_db, 264 + &profile, 265 + ) 266 + .await 267 + { 268 + tracing::warn!( 269 + ?err, 270 + "Failed to update local profile after avatar upload, will be synced via Jetstream" 271 + ); 272 + } 273 + } 274 + 226 275 // Redirect back to settings page 227 276 Ok(( 228 277 StatusCode::OK, ··· 287 336 "image/png", 288 337 ) 289 338 .await?; 339 + 340 + // Store the banner image locally in content storage immediately 341 + // This ensures the image is available when the page reloads, without waiting for Jetstream 342 + let banner_cid = &blob.inner.ref_.link; 343 + let image_path = format!("{}.png", banner_cid); 344 + if let Err(err) = web_context 345 + .content_storage 346 + .write_content(&image_path, &processed_data) 347 + .await 348 + { 349 + tracing::warn!( 350 + ?err, 351 + cid = %banner_cid, 352 + "Failed to store banner image locally, will be fetched via Jetstream" 353 + ); 354 + } 355 + 290 356 let profile_aturi = format!( 291 357 "at://{}/events.smokesignal.profile/self", 292 358 current_handle.did ··· 334 400 let put_record_url = format!("{}/xrpc/com.atproto.repo.putRecord", pds_endpoint); 335 401 let record_value = serde_json::to_value(&put_record_request) 336 402 .map_err(|e| BlobError::PutRecordSerializeFailed(e.to_string()))?; 337 - let _response = match post_dpop_json(&web_context.http_client, &dpop_auth, &put_record_url, record_value).await { 403 + let response = match post_dpop_json(&web_context.http_client, &dpop_auth, &put_record_url, record_value).await { 338 404 Ok(response) => response, 339 405 Err(e) => { 340 406 let err_string = e.to_string(); ··· 346 412 } 347 413 }; 348 414 415 + // Update the profile in the local database immediately 416 + // This ensures the profile is visible without waiting for Jetstream 417 + if let Ok(put_response) = serde_json::from_value::<PutRecordSuccessResponse>(response) { 418 + let display_name_for_db = profile 419 + .display_name 420 + .as_ref() 421 + .filter(|s| !s.trim().is_empty()) 422 + .map(|s| s.as_str()) 423 + .unwrap_or(&current_handle.handle); 424 + 425 + if let Err(err) = profile_insert( 426 + &web_context.pool, 427 + &put_response.uri, 428 + &put_response.cid, 429 + &current_handle.did, 430 + display_name_for_db, 431 + &profile, 432 + ) 433 + .await 434 + { 435 + tracing::warn!( 436 + ?err, 437 + "Failed to update local profile after banner upload, will be synced via Jetstream" 438 + ); 439 + } 440 + } 441 + 349 442 Ok(( 350 443 StatusCode::OK, 351 444 HxRetarget("/settings".to_string()), ··· 418 511 let put_record_url = format!("{}/xrpc/com.atproto.repo.putRecord", pds_endpoint); 419 512 let record_value = serde_json::to_value(&put_record_request) 420 513 .map_err(|e| BlobError::PutRecordSerializeFailed(e.to_string()))?; 421 - let _response = match post_dpop_json(&web_context.http_client, &dpop_auth, &put_record_url, record_value).await { 514 + let response = match post_dpop_json(&web_context.http_client, &dpop_auth, &put_record_url, record_value).await { 422 515 Ok(response) => response, 423 516 Err(e) => { 424 517 let err_string = e.to_string(); ··· 430 523 } 431 524 }; 432 525 526 + // Update the profile in the local database immediately 527 + if let Ok(put_response) = serde_json::from_value::<PutRecordSuccessResponse>(response) { 528 + let display_name_for_db = profile 529 + .display_name 530 + .as_ref() 531 + .filter(|s| !s.trim().is_empty()) 532 + .map(|s| s.as_str()) 533 + .unwrap_or(&current_handle.handle); 534 + 535 + if let Err(err) = profile_insert( 536 + &web_context.pool, 537 + &put_response.uri, 538 + &put_response.cid, 539 + &current_handle.did, 540 + display_name_for_db, 541 + &profile, 542 + ) 543 + .await 544 + { 545 + tracing::warn!( 546 + ?err, 547 + "Failed to update local profile after avatar deletion, will be synced via Jetstream" 548 + ); 549 + } 550 + } 551 + 433 552 Ok(( 434 553 StatusCode::OK, 435 554 HxRetarget("/settings".to_string()), ··· 502 621 let put_record_url = format!("{}/xrpc/com.atproto.repo.putRecord", pds_endpoint); 503 622 let record_value = serde_json::to_value(&put_record_request) 504 623 .map_err(|e| BlobError::PutRecordSerializeFailed(e.to_string()))?; 505 - let _response = match post_dpop_json(&web_context.http_client, &dpop_auth, &put_record_url, record_value).await { 624 + let response = match post_dpop_json(&web_context.http_client, &dpop_auth, &put_record_url, record_value).await { 506 625 Ok(response) => response, 507 626 Err(e) => { 508 627 let err_string = e.to_string(); ··· 513 632 return Err(BlobError::PutRecordFailed(e.to_string()).into()); 514 633 } 515 634 }; 635 + 636 + // Update the profile in the local database immediately 637 + if let Ok(put_response) = serde_json::from_value::<PutRecordSuccessResponse>(response) { 638 + let display_name_for_db = profile 639 + .display_name 640 + .as_ref() 641 + .filter(|s| !s.trim().is_empty()) 642 + .map(|s| s.as_str()) 643 + .unwrap_or(&current_handle.handle); 644 + 645 + if let Err(err) = profile_insert( 646 + &web_context.pool, 647 + &put_response.uri, 648 + &put_response.cid, 649 + &current_handle.did, 650 + display_name_for_db, 651 + &profile, 652 + ) 653 + .await 654 + { 655 + tracing::warn!( 656 + ?err, 657 + "Failed to update local profile after banner deletion, will be synced via Jetstream" 658 + ); 659 + } 660 + } 516 661 517 662 Ok(( 518 663 StatusCode::OK,
+25 -2
src/http/handle_settings.rs
··· 31 31 notification::{ 32 32 notification_get, notification_reset_confirmation, notification_set_preference, 33 33 }, 34 - profile::profile_get_by_did, 34 + profile::{profile_get_by_did, profile_insert}, 35 35 webhook::{webhook_delete, webhook_list_by_did, webhook_toggle_enabled, webhook_upsert}, 36 36 }, 37 37 task_webhooks::TaskWork, ··· 886 886 Ok(PutRecordResponse::StrongRef { uri, cid, .. }) => { 887 887 tracing::info!("Profile updated successfully: {} {}", uri, cid); 888 888 889 - // The profile will be picked up by Jetstream and stored in our database 889 + // Update the profile in the local database immediately 890 + // This ensures the profile is visible without waiting for Jetstream 891 + let display_name_for_db = profile 892 + .display_name 893 + .as_ref() 894 + .filter(|s| !s.trim().is_empty()) 895 + .map(|s| s.as_str()) 896 + .unwrap_or(&current_handle.handle); 897 + 898 + if let Err(err) = profile_insert( 899 + &web_context.pool, 900 + &uri, 901 + &cid, 902 + &current_handle.did, 903 + display_name_for_db, 904 + &profile, 905 + ) 906 + .await 907 + { 908 + tracing::warn!( 909 + ?err, 910 + "Failed to update local profile, will be synced via Jetstream" 911 + ); 912 + } 890 913 891 914 // Return updated profile section 892 915 Ok((