1use std::path::{Path, PathBuf};
29use std::sync::Arc;
30use std::time::Duration;
31
32use nostr_sdk::prelude::*;
33use serde::{Deserialize, Serialize};
34
35use crate::crypto;
36use crate::stored_event::event_kind;
37
38const WALLPAPER_DTAG_VALUE: &str = "vector-wallpaper";
39
40const MAX_WALLPAPER_DOWNLOAD_BYTES: u64 = 10 * 1024 * 1024;
44
45pub const MAX_WALLPAPER_BYTES: usize = 5 * 1024 * 1024;
48
49fn wallpapers_dir() -> Result<PathBuf, String> {
52 let npub = crate::db::get_current_account()?;
53 let dir = crate::db::account_dir(&npub)?.join("wallpapers");
54 std::fs::create_dir_all(&dir)
55 .map_err(|e| format!("Failed to create wallpapers dir: {}", e))?;
56 Ok(dir)
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct WallpaperPreview {
61 pub path: String,
63 pub was_animated: bool,
66 pub recommended_dim: u8,
71}
72
73pub fn prepare_wallpaper_preview(
78 chat_npub: &str,
79 file_path: &str,
80) -> Result<WallpaperPreview, String> {
81 let src = Path::new(file_path);
82 let bytes = std::fs::read(src)
83 .map_err(|e| format!("Failed to read image: {}", e))?;
84
85 if bytes.len() > MAX_WALLPAPER_BYTES {
86 return Err(format!(
87 "Image is too large ({} MB). Wallpapers max out at {} MB.",
88 bytes.len() / (1024 * 1024),
89 MAX_WALLPAPER_BYTES / (1024 * 1024),
90 ));
91 }
92
93 let mime = crypto::mime_from_magic_bytes(&bytes).to_string();
94 if !mime.starts_with("image/") {
95 return Err("Wallpapers must be image files.".to_string());
96 }
97
98 let (final_bytes, final_extension, was_animated) =
99 extract_first_frame_if_animated(&bytes, &mime)?;
100
101 let recommended_dim = estimate_brightness_for_white_text(&final_bytes).unwrap_or(50);
106
107 clean_chat_files(chat_npub, FileKind::Preview, None)?;
109
110 let preview = wallpapers_dir()?.join(format!("{}.preview.{}", chat_npub, final_extension));
111 let tmp = preview.with_file_name(format!("{}.preview.{}.tmp", chat_npub, final_extension));
112 std::fs::write(&tmp, &final_bytes)
113 .map_err(|e| format!("Failed to stage preview file: {}", e))?;
114 std::fs::rename(&tmp, &preview)
115 .map_err(|e| format!("Failed to commit preview file: {}", e))?;
116
117 Ok(WallpaperPreview {
118 path: preview.to_string_lossy().to_string(),
119 was_animated,
120 recommended_dim,
121 })
122}
123
124fn estimate_brightness_for_white_text(bytes: &[u8]) -> Option<u8> {
130 use ::image::{GenericImageView, ImageReader};
131 use std::io::Cursor;
132
133 let mut reader = ImageReader::new(Cursor::new(bytes))
134 .with_guessed_format()
135 .ok()?;
136 reader.limits(crate::crypto::bounded_image_limits());
137 let img = reader.decode().ok()?;
138 let thumb = img.thumbnail(64, 64);
139 let rgb = thumb.to_rgb8();
140 let (w, h) = thumb.dimensions();
141 if w == 0 || h == 0 {
142 return None;
143 }
144 let mut sum: u64 = 0;
145 let mut count: u64 = 0;
146 for pixel in rgb.pixels() {
147 let r = pixel[0] as f32;
148 let g = pixel[1] as f32;
149 let b = pixel[2] as f32;
150 let y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
151 sum += y as u64;
152 count += 1;
153 }
154 if count == 0 {
155 return None;
156 }
157 let avg = (sum / count) as f32; let brightness = (95.0 - (avg / 255.0) * 70.0) / 2.0;
167 Some(brightness.clamp(10.0, 50.0) as u8)
168}
169
170pub fn cancel_wallpaper_preview(chat_npub: &str) -> Result<(), String> {
172 clean_chat_files(chat_npub, FileKind::Preview, None)
173}
174
175fn extract_first_frame_if_animated(
180 src: &[u8],
181 mime: &str,
182) -> Result<(Vec<u8>, String, bool), String> {
183 use ::image::{ImageFormat, ImageReader};
184 use std::io::Cursor;
185
186 let is_gif = mime == "image/gif";
187 let is_webp = mime == "image/webp";
188
189 if !is_gif && !is_webp {
190 let ext = crypto::extension_from_mime(mime);
191 return Ok((src.to_vec(), ext, false));
192 }
193
194 let mut reader = ImageReader::with_format(
196 Cursor::new(src),
197 if is_gif { ImageFormat::Gif } else { ImageFormat::WebP },
198 );
199 reader.limits(crate::crypto::bounded_image_limits());
200 let img = reader
201 .decode()
202 .map_err(|e| format!("Failed to decode image: {}", e))?;
203
204 let mut out: Vec<u8> = Vec::new();
205 img.write_to(&mut Cursor::new(&mut out), ImageFormat::Png)
206 .map_err(|e| format!("Failed to re-encode wallpaper: {}", e))?;
207
208 Ok((out, "png".to_string(), true))
209}
210
211#[derive(Copy, Clone)]
212enum FileKind {
213 Preview,
214 Active,
215}
216
217fn clean_chat_files(
221 chat_npub: &str,
222 kind: FileKind,
223 extension_to_skip: Option<&str>,
224) -> Result<(), String> {
225 let dir = wallpapers_dir()?;
226 let entries = match std::fs::read_dir(&dir) {
227 Ok(e) => e,
228 Err(_) => return Ok(()),
229 };
230
231 let preview_prefix = format!("{}.preview.", chat_npub);
232 let active_prefix = format!("{}.", chat_npub);
233
234 for entry in entries.flatten() {
235 let path = entry.path();
236 let name = match path.file_name().and_then(|n| n.to_str()) {
237 Some(n) => n.to_string(),
238 None => continue,
239 };
240 let matches = match kind {
241 FileKind::Preview => name.starts_with(&preview_prefix),
242 FileKind::Active => name.starts_with(&active_prefix) && !name.starts_with(&preview_prefix),
244 };
245 if !matches {
246 continue;
247 }
248 if let Some(ext) = extension_to_skip {
249 let suffix = format!(".{}", ext);
250 if name.ends_with(&suffix) {
251 continue;
252 }
253 }
254 let _ = std::fs::remove_file(&path);
255 }
256 Ok(())
257}
258
259pub async fn publish_wallpaper(chat_npub: &str, blur: u8, dim: u8) -> Result<(), String> {
271 let session = crate::state::SessionGuard::capture();
275
276 let blur = blur.min(30);
277 let dim = dim.min(100);
278 let dir = wallpapers_dir()?;
280 let prefix = format!("{}.preview.", chat_npub);
281 let mut preview_path: Option<PathBuf> = None;
282 for entry in std::fs::read_dir(&dir)
283 .map_err(|e| format!("Wallpapers dir: {}", e))?
284 .flatten()
285 {
286 let p = entry.path();
287 let n = p
288 .file_name()
289 .and_then(|n| n.to_str())
290 .unwrap_or("")
291 .to_string();
292 if n.starts_with(&prefix) {
293 preview_path = Some(p);
294 break;
295 }
296 }
297 let preview = preview_path
298 .ok_or_else(|| "No wallpaper preview to publish. Pick an image first.".to_string())?;
299 let bytes = std::fs::read(&preview)
300 .map_err(|e| format!("Failed to read preview file: {}", e))?;
301
302 let extension = preview
303 .extension()
304 .and_then(|e| e.to_str())
305 .unwrap_or("png")
306 .to_string();
307 let mime = crypto::mime_from_extension(&extension).to_string();
308 let plaintext_hash = crypto::sha256_hex(&bytes);
309
310 let params = crypto::generate_encryption_params();
311 let encrypted = crypto::encrypt_data(&bytes, ¶ms)?;
312
313 let client = crate::state::nostr_client().ok_or("Not logged in")?;
314 let signer = client
315 .signer()
316 .await
317 .map_err(|e| format!("Signer: {}", e))?;
318 let my_pk = crate::state::my_public_key().ok_or("Public key not set")?;
319 let recipient_pk = PublicKey::from_bech32(chat_npub)
324 .map_err(|e| format!("Invalid chat npub: {}", e))?;
325
326 let servers = crate::state::get_blossom_servers();
327
328 let chat_npub_for_progress = chat_npub.to_string();
331 let progress_cb: crate::blossom::ProgressCallback = Arc::new(move |percentage, bytes| {
332 crate::traits::emit_event(
333 "wallpaper_upload_progress",
334 &serde_json::json!({
335 "chat_id": chat_npub_for_progress,
336 "progress": percentage.unwrap_or(0),
337 "bytes": bytes.unwrap_or(0),
338 }),
339 );
340 Ok(())
341 });
342
343 let upload_url = crate::blossom::upload_blob_with_progress_and_failover(
344 signer.clone(),
345 servers,
346 Arc::new(encrypted.clone()),
347 Some(&mime),
348 true,
349 progress_cb,
350 None, None, None, )
354 .await
355 .map_err(|e| format!("Wallpaper upload failed: {}", e))?;
356
357 let created_at = std::time::SystemTime::now()
358 .duration_since(std::time::UNIX_EPOCH)
359 .unwrap()
360 .as_secs();
361 let rumor = EventBuilder::new(Kind::Custom(event_kind::APPLICATION_SPECIFIC), "")
362 .tag(Tag::identifier(WALLPAPER_DTAG_VALUE))
363 .tag(Tag::public_key(recipient_pk))
367 .tag(Tag::custom(
368 TagKind::Custom("url".into()),
369 vec![upload_url.clone()],
370 ))
371 .tag(Tag::custom(
372 TagKind::Custom("decryption-key".into()),
373 vec![params.key.clone()],
374 ))
375 .tag(Tag::custom(
376 TagKind::Custom("decryption-nonce".into()),
377 vec![params.nonce.clone()],
378 ))
379 .tag(Tag::custom(
380 TagKind::Custom("x".into()),
381 vec![plaintext_hash.clone()],
382 ))
383 .tag(Tag::custom(
384 TagKind::Custom("m".into()),
385 vec![mime.clone()],
386 ))
387 .tag(Tag::custom(
388 TagKind::Custom("size".into()),
389 vec![encrypted.len().to_string()],
390 ))
391 .tag(Tag::custom(
392 TagKind::Custom("blur".into()),
393 vec![blur.to_string()],
394 ))
395 .tag(Tag::custom(
396 TagKind::Custom("dim".into()),
397 vec![dim.to_string()],
398 ))
399 .custom_created_at(Timestamp::from(created_at))
400 .build(my_pk);
401
402 let pending_id = format!("pending-wallpaper-{}", created_at);
410 let send_config = crate::sending::SendConfig {
411 max_send_attempts: 3,
412 retry_delay: std::time::Duration::from_secs(2),
413 self_send: true,
414 ..Default::default()
415 };
416 let send_callback: Arc<dyn crate::sending::SendCallback> =
417 Arc::new(crate::sending::NoOpSendCallback);
418 if let Err(e) = crate::sending::send_rumor_dm(
419 chat_npub, &pending_id, rumor.clone(), &send_config, send_callback,
420 ).await {
421 log_warn!("[Wallpaper] send_rumor_dm to {} failed: {}", chat_npub, e);
422 return Err(format!(
423 "Couldn't send the wallpaper. Check that the relays you and your contact share are reachable, then try again. ({})",
424 e
425 ));
426 }
427 log_info!("[Wallpaper] rumor delivered to {}", chat_npub);
428
429 if !session.is_valid() {
433 return Ok(());
434 }
435
436 let active = wallpapers_dir()?.join(format!("{}.{}", chat_npub, extension));
438 clean_chat_files(chat_npub, FileKind::Active, None)?;
439 std::fs::rename(&preview, &active)
440 .map_err(|e| format!("Failed to promote preview: {}", e))?;
441 let active_str = active.to_string_lossy().to_string();
442
443 let me_npub = my_pk.to_bech32().unwrap_or_default();
444
445 let (slim, prev_url, prev_uploader) = {
448 let mut state = crate::state::STATE.lock().await;
449 let prev = state.get_chat(chat_npub).map(|c| {
450 (c.wallpaper_url.clone(), c.wallpaper_uploader.clone())
451 });
452 if let Some(chat) = state.get_chat_mut(chat_npub) {
453 chat.wallpaper_path = active_str.clone();
454 chat.wallpaper_ts = created_at;
455 chat.wallpaper_blur = blur;
456 chat.wallpaper_dim = dim;
457 chat.wallpaper_url = upload_url.clone();
458 chat.wallpaper_uploader = me_npub.clone();
459 }
460 let slim = state
461 .get_chat(chat_npub)
462 .map(|c| crate::db::chats::SlimChatDB::from_chat(c, &state.interner));
463 let (pu, puploader) = prev.unwrap_or_default();
464 (slim, pu, puploader)
465 };
466 if let Some(slim) = slim {
467 if let Err(e) = crate::db::chats::save_slim_chat(&slim) {
468 log_warn!("[Wallpaper] save_slim_chat failed for {}: {}", chat_npub, e);
469 }
470 }
471
472 if !prev_url.is_empty() && prev_uploader == me_npub {
478 let signer_clone = signer.clone();
479 let prev_url_clone = prev_url.clone();
480 tokio::spawn(async move {
481 if let Err(e) =
482 crate::blossom::delete_blob_by_url(signer_clone, &prev_url_clone).await
483 {
484 log_warn!(
485 "[Wallpaper] DELETE prev blob {} failed: {}",
486 prev_url_clone, e
487 );
488 }
489 });
490 }
491
492 let event_id = rumor.id.ok_or("Rumor missing id")?.to_hex();
493 let inserted = match crate::db::events::save_system_event_by_id(
494 &event_id,
495 chat_npub,
496 crate::stored_event::SystemEventType::WallpaperChanged,
497 &me_npub,
498 Some("You"),
499 )
500 .await
501 {
502 Ok(b) => b,
503 Err(e) => {
504 log_warn!("[Wallpaper] save_system_event_by_id failed for {}: {}", event_id, e);
505 false
506 }
507 };
508 if inserted {
509 crate::traits::emit_event("system_event", &serde_json::json!({
510 "conversation_id": chat_npub,
511 "event_id": event_id,
512 "event_type": crate::stored_event::SystemEventType::WallpaperChanged.as_u8(),
513 "member_pubkey": me_npub,
514 "member_name": "You",
515 }));
516 } else {
517 log_warn!("[Wallpaper] system event {} was not inserted (already exists or save failed)", event_id);
518 }
519
520 crate::traits::emit_event(
521 "wallpaper_updated",
522 &serde_json::json!({
523 "chat_id": chat_npub,
524 "path": active_str,
525 "ts": created_at,
526 "blur": blur,
527 "dim": dim,
528 "by_npub": me_npub,
529 "event_id": event_id,
530 }),
531 );
532
533 Ok(())
534}
535
536#[allow(clippy::too_many_arguments)]
542pub async fn apply_received_wallpaper(
543 chat_npub: &str,
544 sender_npub: &str,
545 created_at: u64,
546 url: &str,
547 decryption_key: &str,
548 decryption_nonce: &str,
549 plaintext_hash: Option<&str>,
550 mime: Option<&str>,
551 blur: Option<u8>,
552 dim: Option<u8>,
553 rumor_event_id: &str,
554) -> Result<(), String> {
555 let session = crate::state::SessionGuard::capture();
559
560 let blur = blur.unwrap_or(0).min(30);
561 let dim = dim.unwrap_or(50).min(100);
562 {
565 let state = crate::state::STATE.lock().await;
566 if let Some(chat) = state.get_chat(chat_npub) {
567 if chat.wallpaper_ts >= created_at {
568 return Ok(());
569 }
570 }
571 }
572
573 if url.is_empty() {
576 clean_chat_files(chat_npub, FileKind::Active, None)?;
577 let (slim, prev_url, prev_uploader) = {
578 let mut state = crate::state::STATE.lock().await;
579 let prev = state.get_chat(chat_npub).map(|c| {
580 (c.wallpaper_url.clone(), c.wallpaper_uploader.clone())
581 });
582 if let Some(chat) = state.get_chat_mut(chat_npub) {
583 chat.wallpaper_path = String::new();
584 chat.wallpaper_url = String::new();
585 chat.wallpaper_uploader = String::new();
586 chat.wallpaper_ts = created_at;
587 chat.wallpaper_blur = blur;
588 chat.wallpaper_dim = dim;
589 }
590 let slim = state
591 .get_chat(chat_npub)
592 .map(|c| crate::db::chats::SlimChatDB::from_chat(c, &state.interner));
593 let (pu, puploader) = prev.unwrap_or_default();
594 (slim, pu, puploader)
595 };
596 if let Some(slim) = slim {
597 let _ = crate::db::chats::save_slim_chat(&slim);
598 }
599 delete_prior_blob_if_ours(&prev_url, &prev_uploader).await;
600 emit_wallpaper_removed(chat_npub, sender_npub, created_at, rumor_event_id).await;
601 return Ok(());
602 }
603
604 let mime_str = mime.unwrap_or("image/png").to_string();
605 let extension = crypto::extension_from_mime(&mime_str);
606
607 crate::net::validate_url_not_private(url)?;
610
611 let http = crate::net::build_http_client(Duration::from_secs(30))?;
612 let resp = http
613 .get(url)
614 .send()
615 .await
616 .map_err(|e| format!("Wallpaper download failed: {}", e))?;
617 if !resp.status().is_success() {
618 return Err(format!("Wallpaper HTTP {}", resp.status()));
619 }
620 if let Some(len) = resp.content_length() {
623 if len > MAX_WALLPAPER_DOWNLOAD_BYTES {
624 return Err("Wallpaper too large".to_string());
625 }
626 }
627 let mut resp = resp;
628 let mut bytes: Vec<u8> = Vec::new();
629 while let Some(chunk) = resp
630 .chunk()
631 .await
632 .map_err(|e| format!("Read body: {}", e))?
633 {
634 bytes.extend_from_slice(&chunk);
635 if bytes.len() as u64 > MAX_WALLPAPER_DOWNLOAD_BYTES {
636 return Err("Wallpaper too large".to_string());
637 }
638 }
639
640 let plaintext = crypto::decrypt_data(&bytes, decryption_key, decryption_nonce)?;
641
642 if let Some(want_hash) = plaintext_hash {
643 let got_hash = crypto::sha256_hex(&plaintext);
644 if !got_hash.eq_ignore_ascii_case(want_hash) {
645 return Err("Wallpaper integrity check failed".to_string());
646 }
647 }
648
649 if !session.is_valid() {
653 return Ok(());
654 }
655
656 let active = wallpapers_dir()?.join(format!("{}.{}", chat_npub, extension));
657 clean_chat_files(chat_npub, FileKind::Active, None)?;
658 let tmp = active.with_file_name(format!("{}.{}.tmp", chat_npub, extension));
659 std::fs::write(&tmp, &plaintext).map_err(|e| format!("Write wallpaper: {}", e))?;
660 std::fs::rename(&tmp, &active).map_err(|e| format!("Commit wallpaper: {}", e))?;
661 let active_str = active.to_string_lossy().to_string();
662
663 let (slim, prev_url, prev_uploader) = {
667 let mut state = crate::state::STATE.lock().await;
668 let prev = state.get_chat(chat_npub).map(|c| {
669 (c.wallpaper_url.clone(), c.wallpaper_uploader.clone())
670 });
671 if let Some(chat) = state.get_chat_mut(chat_npub) {
672 chat.wallpaper_path = active_str.clone();
673 chat.wallpaper_ts = created_at;
674 chat.wallpaper_blur = blur;
675 chat.wallpaper_dim = dim;
676 chat.wallpaper_url = url.to_string();
677 chat.wallpaper_uploader = sender_npub.to_string();
678 }
679 let slim = state
680 .get_chat(chat_npub)
681 .map(|c| crate::db::chats::SlimChatDB::from_chat(c, &state.interner));
682 let (pu, puploader) = prev.unwrap_or_default();
683 (slim, pu, puploader)
684 };
685 if let Some(slim) = slim {
686 let _ = crate::db::chats::save_slim_chat(&slim);
687 }
688
689 if !prev_url.is_empty() {
690 let me_npub = crate::state::my_public_key()
691 .and_then(|pk| pk.to_bech32().ok())
692 .unwrap_or_default();
693 if !me_npub.is_empty() && prev_uploader == me_npub {
694 if let Some(client) = crate::state::nostr_client() {
695 if let Ok(signer) = client.signer().await {
696 let prev_url_clone = prev_url.clone();
697 tokio::spawn(async move {
698 if let Err(e) =
699 crate::blossom::delete_blob_by_url(signer, &prev_url_clone).await
700 {
701 log_warn!(
702 "[Wallpaper] DELETE prev blob {} failed: {}",
703 prev_url_clone, e
704 );
705 }
706 });
707 }
708 }
709 }
710 }
711
712 let sender_display = {
716 let state = crate::state::STATE.lock().await;
717 state
718 .get_profile(sender_npub)
719 .and_then(|p| {
720 if !p.nickname.is_empty() {
721 Some(p.nickname.to_string())
722 } else if !p.name.is_empty() {
723 Some(p.name.to_string())
724 } else {
725 None
726 }
727 })
728 .unwrap_or_else(|| sender_npub.to_string())
729 };
730 let inserted = crate::db::events::save_system_event_by_id(
731 rumor_event_id,
732 chat_npub,
733 crate::stored_event::SystemEventType::WallpaperChanged,
734 sender_npub,
735 Some(&sender_display),
736 )
737 .await
738 .unwrap_or(false);
739 if inserted {
740 crate::traits::emit_event("system_event", &serde_json::json!({
741 "conversation_id": chat_npub,
742 "event_id": rumor_event_id,
743 "event_type": crate::stored_event::SystemEventType::WallpaperChanged.as_u8(),
744 "member_pubkey": sender_npub,
745 "member_name": sender_display,
746 }));
747 }
748
749 crate::traits::emit_event(
750 "wallpaper_updated",
751 &serde_json::json!({
752 "chat_id": chat_npub,
753 "path": active_str,
754 "ts": created_at,
755 "blur": blur,
756 "dim": dim,
757 "by_npub": sender_npub,
758 "event_id": rumor_event_id,
759 }),
760 );
761
762 Ok(())
763}
764
765async fn delete_prior_blob_if_ours(prev_url: &str, prev_uploader: &str) {
769 if prev_url.is_empty() {
770 return;
771 }
772 let me_npub = crate::state::my_public_key()
773 .and_then(|pk| pk.to_bech32().ok())
774 .unwrap_or_default();
775 if me_npub.is_empty() || prev_uploader != me_npub {
776 return;
777 }
778 if let Some(client) = crate::state::nostr_client() {
779 if let Ok(signer) = client.signer().await {
780 let prev_url = prev_url.to_string();
781 tokio::spawn(async move {
782 if let Err(e) = crate::blossom::delete_blob_by_url(signer, &prev_url).await {
783 log_warn!("[Wallpaper] DELETE prev blob {} failed: {}", prev_url, e);
784 }
785 });
786 }
787 }
788}
789
790async fn emit_wallpaper_removed(
793 chat_npub: &str,
794 by_npub: &str,
795 created_at: u64,
796 event_id: &str,
797) {
798 let me_npub = crate::state::my_public_key()
799 .and_then(|pk| pk.to_bech32().ok())
800 .unwrap_or_default();
801 let display = if by_npub == me_npub {
802 "You".to_string()
803 } else {
804 let state = crate::state::STATE.lock().await;
805 state
806 .get_profile(by_npub)
807 .and_then(|p| {
808 if !p.nickname.is_empty() {
809 Some(p.nickname.to_string())
810 } else if !p.name.is_empty() {
811 Some(p.name.to_string())
812 } else {
813 None
814 }
815 })
816 .unwrap_or_else(|| by_npub.to_string())
817 };
818 let inserted = crate::db::events::save_system_event_by_id(
819 event_id,
820 chat_npub,
821 crate::stored_event::SystemEventType::WallpaperRemoved,
822 by_npub,
823 Some(&display),
824 )
825 .await
826 .unwrap_or(false);
827 if inserted {
828 crate::traits::emit_event("system_event", &serde_json::json!({
829 "conversation_id": chat_npub,
830 "event_id": event_id,
831 "event_type": crate::stored_event::SystemEventType::WallpaperRemoved.as_u8(),
832 "member_pubkey": by_npub,
833 "member_name": display,
834 }));
835 }
836 crate::traits::emit_event(
837 "wallpaper_updated",
838 &serde_json::json!({
839 "chat_id": chat_npub,
840 "path": "",
841 "ts": created_at,
842 "blur": 0,
843 "dim": 50,
844 "by_npub": by_npub,
845 "event_id": event_id,
846 }),
847 );
848}
849
850pub async fn remove_wallpaper(chat_npub: &str) -> Result<(), String> {
855 let session = crate::state::SessionGuard::capture();
856
857 let my_pk = crate::state::my_public_key().ok_or("Public key not set")?;
858 let recipient_pk = PublicKey::from_bech32(chat_npub)
859 .map_err(|e| format!("Invalid chat npub: {}", e))?;
860
861 let created_at = std::time::SystemTime::now()
862 .duration_since(std::time::UNIX_EPOCH)
863 .unwrap()
864 .as_secs();
865 let rumor = EventBuilder::new(Kind::Custom(event_kind::APPLICATION_SPECIFIC), "")
867 .tag(Tag::identifier(WALLPAPER_DTAG_VALUE))
868 .tag(Tag::public_key(recipient_pk))
869 .custom_created_at(Timestamp::from(created_at))
870 .build(my_pk);
871
872 let pending_id = format!("pending-wallpaper-rm-{}", created_at);
873 let send_config = crate::sending::SendConfig {
874 max_send_attempts: 3,
875 retry_delay: std::time::Duration::from_secs(2),
876 self_send: true,
877 ..Default::default()
878 };
879 let send_callback: Arc<dyn crate::sending::SendCallback> =
880 Arc::new(crate::sending::NoOpSendCallback);
881 if let Err(e) = crate::sending::send_rumor_dm(
882 chat_npub, &pending_id, rumor.clone(), &send_config, send_callback,
883 ).await {
884 log_warn!("[Wallpaper] removal send to {} failed: {}", chat_npub, e);
885 return Err(format!(
886 "Couldn't remove the wallpaper. Check that the relays you and your contact share are reachable, then try again. ({})",
887 e
888 ));
889 }
890
891 if !session.is_valid() {
894 return Ok(());
895 }
896
897 clean_chat_files(chat_npub, FileKind::Active, None)?;
898 let me_npub = my_pk.to_bech32().unwrap_or_default();
899 let (slim, prev_url, prev_uploader) = {
900 let mut state = crate::state::STATE.lock().await;
901 let prev = state.get_chat(chat_npub).map(|c| {
902 (c.wallpaper_url.clone(), c.wallpaper_uploader.clone())
903 });
904 if let Some(chat) = state.get_chat_mut(chat_npub) {
905 chat.wallpaper_path = String::new();
906 chat.wallpaper_url = String::new();
907 chat.wallpaper_uploader = String::new();
908 chat.wallpaper_ts = created_at;
909 }
910 let slim = state
911 .get_chat(chat_npub)
912 .map(|c| crate::db::chats::SlimChatDB::from_chat(c, &state.interner));
913 let (pu, puploader) = prev.unwrap_or_default();
914 (slim, pu, puploader)
915 };
916 if let Some(slim) = slim {
917 if let Err(e) = crate::db::chats::save_slim_chat(&slim) {
918 log_warn!("[Wallpaper] save_slim_chat (removal) failed for {}: {}", chat_npub, e);
919 }
920 }
921
922 delete_prior_blob_if_ours(&prev_url, &prev_uploader).await;
923
924 let event_id = rumor.id.ok_or("Rumor missing id")?.to_hex();
925 emit_wallpaper_removed(chat_npub, &me_npub, created_at, &event_id).await;
926
927 Ok(())
928}