Skip to content

Include decrypted image in push notifications#419

Merged
benthecarman merged 2 commits intosledtools:masterfrom
benthecarman:pic-notif
Mar 4, 2026
Merged

Include decrypted image in push notifications#419
benthecarman merged 2 commits intosledtools:masterfrom
benthecarman:pic-notif

Conversation

@benthecarman
Copy link
Copy Markdown
Collaborator

@benthecarman benthecarman commented Mar 4, 2026

Summary

  • When a push notification contains an image message, the NSE now downloads the encrypted image, decrypts it via MDK's media manager, and attaches it as a UNNotificationAttachment so iOS displays the image thumbnail inline
  • Downloads capped at 10MB to stay within NSE memory limits; any failure gracefully falls back to text-only notification
  • Adds ureq (lightweight blocking HTTP client) to pika-nse for the download

Test plan

  • Send an image in a chat and verify the push notification shows the image thumbnail
  • Send a large image (>10MB encrypted) and verify it gracefully falls back to "Sent a photo"
  • Send a text-only message and verify notifications still work normally
  • Send a video/audio/file and verify they still show text labels without attachment

🤖 Generated with Claude Code


Open with Devin

Summary by CodeRabbit

  • New Features

    • iOS push notifications can include decrypted image thumbnails as attachments.
    • Images referenced in notifications are automatically downloaded, decrypted, and attached when available.
    • A 10 MB size limit for notification images is enforced to preserve performance.
  • Bug Fixes

    • If image retrieval or decryption fails, notifications gracefully fall back to text-only content.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

Adds end-to-end image support for rich push notifications: adds ureq for HTTP image download, a NotifMedia abstraction and image download/decrypt logic with a 10 MB limit in the NSE, exposes image_data on notification content, and attaches decrypted thumbnails in the iOS Notification Service.

Changes

Cohort / File(s) Summary
Dependency Addition
crates/pika-nse/Cargo.toml
Added ureq = "3" dependency for HTTP image downloads.
NSE — Image handling & media abstractions
crates/pika-nse/src/lib.rs
Added NotifMedia<'_> struct and replaced notif_media_kind() with notif_media(); added MAX_NSE_IMAGE_BYTES (10 MB); added download_and_decrypt_image(); threaded image_data: Option<Vec<u8>> into PushNotificationContent; updated decrypt flow and tests accordingly.
iOS — Notification attachment
ios/NotificationService/NotificationService.swift
Added createImageAttachment(data:) helper and attach decrypted imageData as a UNNotificationAttachment when present.

Sequence Diagram

sequenceDiagram
    participant iOS as NotificationService
    participant NSE as NSE (Rust)
    participant ImgSrc as ImageServer
    participant FS as FileSystem

    iOS->>NSE: request decrypt_push_notification(encrypted_payload)
    NSE->>NSE: parse tags -> notif_media(tags) -> NotifMedia{kind, tag}
    alt media is Image
        NSE->>ImgSrc: download image (ureq, <= 10 MB)
        ImgSrc-->>NSE: image bytes
        NSE->>NSE: download_and_decrypt_image(bytes)
        NSE-->>iOS: PushNotificationContent(image_data)
    else no valid image
        NSE-->>iOS: PushNotificationContent(no image_data)
    end
    iOS->>iOS: if imageData -> createImageAttachment(data)
    iOS->>FS: write temp file
    FS-->>iOS: file URL
    iOS->>iOS: create UNNotificationAttachment & assign to content.attachments
    iOS->>iOS: finalize and deliver notification
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I nibble bytes from distant streams,

I decrypt the picture of your dreams,
ten meg hops and a thumbnail bloom,
I tuck it softly in your room,
and thump — your lockscreen wakes with tunes.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly and accurately summarizes the primary change: adding decrypted image support to push notifications across the Rust backend and iOS frontend.
Docstring Coverage ✅ Passed Docstring coverage is 93.75% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

When a push notification contains an image message, the NSE now
downloads and decrypts the image via MDK's media manager and attaches
it as a UNNotificationAttachment so iOS displays the image thumbnail
inline in the notification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
crates/pika-nse/src/lib.rs (1)

256-257: ⚠️ Potential issue | 🟠 Major

Configure bounded request timeouts and HTTPS-only for NSE downloads.

The ureq::get().call() uses unbounded defaults which can stall NSE processing indefinitely, and permits HTTP requests which could leak encrypted content over unencrypted channels.

🔧 Proposed fix using ureq 3.x config API
+use std::time::Duration;
+
 fn download_and_decrypt_image(
     mdk: &mdk_support::PikaMdk,
     mls_group_id: &mdk_storage_traits::GroupId,
     imeta_tag: &nostr::Tag,
 ) -> Option<Vec<u8>> {
     let manager = mdk.media_manager(mls_group_id.clone());
     let reference = manager.parse_imeta_tag(imeta_tag).ok()?;

-    let response = ureq::get(&reference.url).call().ok()?;
+    let response = ureq::get(&reference.url)
+        .config()
+        .https_only(true)
+        .timeout_global(Some(Duration::from_secs(8)))
+        .build()
+        .call()
+        .ok()?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/pika-nse/src/lib.rs` around lines 256 - 257, The code currently calls
ureq::get(&reference.url).call().ok()? which uses unbounded timeouts and allows
HTTP; change to require HTTPS by validating reference.url.scheme() == "https"
(return None or error otherwise), create a configured ureq Agent with
ureq::AgentBuilder::new().timeout_connect(Duration::from_secs(...)).timeout_read(Duration::from_secs(...)).timeout_write(Duration::from_secs(...)).build(),
then replace ureq::get(&reference.url).call() with
agent.get(&reference.url).call().ok()? so downloads use bounded timeouts and
only HTTPS.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/pika-nse/src/lib.rs`:
- Around line 256-257: The code currently calls
ureq::get(&reference.url).call().ok()? which uses unbounded timeouts and allows
HTTP; change to require HTTPS by validating reference.url.scheme() == "https"
(return None or error otherwise), create a configured ureq Agent with
ureq::AgentBuilder::new().timeout_connect(Duration::from_secs(...)).timeout_read(Duration::from_secs(...)).timeout_write(Duration::from_secs(...)).build(),
then replace ureq::get(&reference.url).call() with
agent.get(&reference.url).call().ok()? so downloads use bounded timeouts and
only HTTPS.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3f1366ad-d192-4ba9-9ea0-e00bdd446f51

📥 Commits

Reviewing files that changed from the base of the PR and between a825fa7 and 02dfa8b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • crates/pika-nse/Cargo.toml
  • crates/pika-nse/src/lib.rs
  • ios/NotificationService/NotificationService.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • ios/NotificationService/NotificationService.swift

- Configure ureq with https_only and 8s global timeout so a slow
  download cannot consume the NSE's time budget
- Detect actual image format via CGImageSource and pass the correct UTI
  to UNNotificationAttachment instead of hardcoding .jpg

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
ios/NotificationService/NotificationService.swift (1)

156-173: Consider pruning stale temp files to avoid gradual storage buildup.

Line 171 creates a new unique file each time, but old files in notif-images are never removed. A small pre-write cleanup pass (e.g., remove files older than N hours) would keep NSE container storage bounded.

Suggested lightweight cleanup pattern
 private static func createImageAttachment(data: Data) -> UNNotificationAttachment? {
     let tmpDir = FileManager.default.temporaryDirectory
         .appendingPathComponent("notif-images", isDirectory: true)
     try? FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true)

+    // Best-effort cleanup of stale temp files.
+    if let files = try? FileManager.default.contentsOfDirectory(
+        at: tmpDir,
+        includingPropertiesForKeys: [.contentModificationDateKey],
+        options: [.skipsHiddenFiles]
+    ) {
+        let cutoff = Date().addingTimeInterval(-24 * 60 * 60)
+        for file in files {
+            if let values = try? file.resourceValues(forKeys: [.contentModificationDateKey]),
+               let modified = values.contentModificationDate,
+               modified < cutoff {
+                try? FileManager.default.removeItem(at: file)
+            }
+        }
+    }
+
     // Detect the actual image type so iOS can decode it correctly.
     var uti: String = UTType.jpeg.identifier
     var ext: String = "jpg"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/NotificationService/NotificationService.swift` around lines 156 - 173,
Before writing the new image file in NotificationService.swift (around the
tmpDir / fileURL / data.write block), add a lightweight cleanup that lists
contents of tmpDir via FileManager, checks each file's creation/modification
date, and removes files older than a configurable threshold (e.g., N hours)
using removeItem(at:). Perform this pass with try? or do/catch to avoid crashing
the service, ignore failures per-file, and run it immediately before
creating/appending the new UUID file so storage is bounded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/pika-nse/src/lib.rs`:
- Around line 262-263: The NSE currently performs network I/O directly from
untrusted message metadata by calling agent.get(&reference.url).call(); before
doing that, validate reference.url's host against a trusted media-host allowlist
(or rewrite the URL to your trusted media proxy endpoint) and reject or return
None for disallowed hosts; specifically, add a guard near where response is
created that parses reference.url, checks the domain against the allowlist (or
constructs a proxy URL mapping), and only then calls agent.get(...) (otherwise
early-return None or an error) so that agent.get is never invoked on arbitrary
third-party hosts.

---

Nitpick comments:
In `@ios/NotificationService/NotificationService.swift`:
- Around line 156-173: Before writing the new image file in
NotificationService.swift (around the tmpDir / fileURL / data.write block), add
a lightweight cleanup that lists contents of tmpDir via FileManager, checks each
file's creation/modification date, and removes files older than a configurable
threshold (e.g., N hours) using removeItem(at:). Perform this pass with try? or
do/catch to avoid crashing the service, ignore failures per-file, and run it
immediately before creating/appending the new UUID file so storage is bounded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3771a13e-b5c4-4300-9542-32de0181526b

📥 Commits

Reviewing files that changed from the base of the PR and between 02dfa8b and 23166e2.

📒 Files selected for processing (2)
  • crates/pika-nse/src/lib.rs
  • ios/NotificationService/NotificationService.swift

@benthecarman benthecarman merged commit de02479 into sledtools:master Mar 4, 2026
18 checks passed
@benthecarman benthecarman deleted the pic-notif branch March 4, 2026 18:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant