Skip to content

edit message bindings#3346

Open
mchenani wants to merge 2 commits intomc/edit-message-corefrom
mc/edit-message-bindings
Open

edit message bindings#3346
mchenani wants to merge 2 commits intomc/edit-message-corefrom
mc/edit-message-bindings

Conversation

@mchenani
Copy link
Copy Markdown
Contributor

@mchenani mchenani commented Mar 17, 2026

TL;DR

Added message editing functionality to XMTP mobile, node, and wasm bindings with support for streaming edit notifications and proper handling of reply message edits.

What changed?

  • Added FfiEditMessage struct and EditMessage content type support in mobile bindings
  • Implemented edit_message() method on conversations to edit messages by ID
  • Added stream_message_edits() functionality to receive real-time edit notifications via FfiMessageEditCallback
  • Enhanced FfiDecodedMessage with edited_at_ns timestamp and is_edited() boolean flag
  • Added logic to preserve reply structure when editing reply messages - edited replies maintain their in_reply_to references
  • Implemented encode/decode functions for EditMessage content type
  • Added comprehensive test coverage for edit scenarios including multiple edits, unauthorized edits, edit-then-delete chains, and reply editing
  • Extended node.js bindings with editMessage() and streamMessageEdits() methods
  • Added WASM bindings support for editMessage(), deleteMessage(), and streamMessageEdits()
  • Updated decoded message handling to show edited content while preserving original message structure

How to test?

  1. Send a text message in a conversation
  2. Call edit_message(message_id, new_content) to edit it
  3. Verify the message shows is_edited() == true and has edited_at_ns timestamp
  4. Test editing reply messages - they should maintain their reply structure
  5. Set up stream_message_edits() callback to receive real-time edit notifications
  6. Test edge cases: editing deleted messages (should fail), editing reactions (should fail), multiple edits on same message

Why make this change?

This enables users to correct typos, update information, or modify their messages after sending, which is a standard feature in modern messaging applications. The implementation properly handles complex scenarios like editing replies while preserving message threading, and provides real-time notifications so all participants can see message updates immediately.

Note

Add edit message bindings for mobile, Node, and WASM clients

  • Adds edit_message and stream_message_edits methods to conversation and conversations bindings across mobile (uniffi), Node (napi), and WASM targets.
  • Introduces FfiEditMessage / FfiDecodedMessage edit fields (edited_at_ns, is_edited) in the mobile FFI layer, with equivalent fields in Node and WASM DecodedMessage.
  • Decoded messages with edits surface the edited content and content type; reply semantics are preserved by wrapping non-reply edited content back into the original reply structure.
  • Adds encode_edit_message / decode_edit_message helpers for serializing edit payloads to/from EncodedContent bytes.
  • Covers the new surface with broad tests across all three binding targets, including authorization, multi-edit, reply chains, delete-after-edit, and filtered message list behavior.

Macroscope summarized 1f95d4c.

Copy link
Copy Markdown
Contributor Author

mchenani commented Mar 17, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more


How to use the Graphite Merge Queue

Add the label mergequeue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@claude
Copy link
Copy Markdown

claude bot commented Mar 17, 2026

Claude finished @mchenani's task —— View job


Code Review Complete

Review Date: March 18, 2026


Summary

This PR adds comprehensive message editing functionality across all three bindings (mobile/uniffi, node/napi, WASM). The implementation is well-structured with strong test coverage, but there are important security and edge case concerns that should be addressed.


Critical Issues

1. Reply Structure Preservation Logic - Potential Data Loss 🔴

Location: bindings/mobile/src/message.rs:1318-1355, similar in node/WASM

The logic that preserves reply structure when editing has a subtle issue:

// Preserve in_reply_to from the original Reply
if let (MessageBody::Reply(original_reply), MessageBody::Reply(edited_reply)) =
    (&msg.content, &mut edited_body)
{
    edited_reply.in_reply_to = original_reply.in_reply_to.clone(); // ✅ Good
}
// Wrap non-Reply edited content in Reply if original was a Reply
else if let MessageBody::Reply(original_reply) = &msg.content {
    edited_body = MessageBody::Reply(ProcessedReply {
        in_reply_to: original_reply.in_reply_to.clone(),
        content: Box::new(edited_body),
        reference_id: original_reply.reference_id.clone(),
    });
    final_content_type = metadata.content_type.clone();
}

Problem: When editing a reply with plain text, the reference_id is preserved from the original, but the actual edited_content that gets decoded might have a different reference field if someone manually constructs an EditMessage with mismatched data. This could cause display inconsistencies.

Recommendation: Add validation to ensure the reference_id in edited content matches the original reply's reference when wrapping non-Reply content.


2. Missing Validation - Empty Edited Content 🟡

Location: bindings/mobile/src/mls.rs (inferred from tests at line 2284-2302)

The test test_edit_message_empty_content_ffi checks that editing with empty content fails, but I don't see explicit validation in the FFI layer - it appears to rely on core validation.

Question: Is the validation in xmtp_mls::groups::MlsGroup::edit_message? If not, the FFI layer should add explicit checks:

pub fn edit_message(&self, message_id: Vec<u8>, new_content: Vec<u8>) -> Result<Vec<u8>, FfiError> {
    if new_content.is_empty() {
        return Err(FfiError::Generic("Edited content cannot be empty".to_string()));
    }
    // ... rest of implementation
}

3. Low Test Coverage in Critical Paths 🟡

Per Codecov Report:

  • bindings/mobile/src/mls.rs: 59.52% coverage, 17 lines missing
  • bindings/mobile/src/message.rs: 84.31% coverage, 8 lines missing

The coverage gaps are concerning for new functionality. Based on the file structure:

  • Missing coverage likely includes error paths in edit_message() and stream_message_edits()
  • The FfiEditMessage conversion logic might not be fully tested

Recommendation:

  • Add tests for error scenarios (malformed EditMessage proto, invalid message IDs)
  • Add tests for streaming when no edits occur
  • Test concurrent edits from multiple users

Security Concerns

4. Authorization Check Location 🔴

The tests verify that only the sender can edit (line 1690-1732), but where is this check enforced?

Looking at the FFI code, edit_message calls:

let edit_id = group.edit_message(message_id_bytes, new_content.encode_to_vec())

Critical Question: Is authorization checked in the core MlsGroup::edit_message() method? If the check is only in the bindings layer, a malicious actor could bypass it by calling core functions directly.

Verification Needed: Confirm authorization is enforced in xmtp_mls/src/groups/mod.rs (or equivalent core file), not just in bindings.


5. Race Condition on Concurrent Edits 🟡

Scenario: User A and User B both try to edit the same message simultaneously.

Current behavior (inferred from tests):

  • Last edit wins (test test_edit_message_multiple_times_ffi shows sequential edits work)
  • No test for concurrent conflicting edits from different users

Potential issue:

  • If two users edit the same message, both edits get stored, but only the last synced edit is displayed
  • No conflict resolution or warning to users

Recommendation: Add a test case for concurrent edits and document the expected behavior.


Performance Considerations

6. Streaming Efficiency 🟡

Location: bindings/wasm/src/conversations.rs:704-718

The stream_message_edits implementation creates a closure that:

  1. Decodes the protobuf
  2. Converts to FFI type via TryFrom
  3. Calls JavaScript callback

Concern: For high-frequency edit streams (e.g., collaborative editing), this could create significant overhead.

Recommendation:

  • Consider batching edit notifications (e.g., buffer 100ms of edits)
  • Add performance metrics/logging for edit stream processing

7. Missing Streaming Error Handling 🟡

Location: bindings/node/src/conversations/streams.rs:241-268

pub async fn stream_message_edits(
    &self,
    callback: ThreadsafeFunction<DecodedMessage, ()>,
) -> Result<StreamCloser> {
    let stream_closer = RustXmtpClient::stream_message_edits_with_callback(
        self.inner_client.clone(),
        move |message| match message {
            Ok(decoded_message) => match DecodedMessage::try_from(decoded_message) {
                Ok(msg) => {
                    let _ = callback.call(Ok(msg), ThreadsafeFunctionCallMode::Blocking);
                }
                Err(e) => {
                    let _ = callback.call(Err(e), ThreadsafeFunctionCallMode::Blocking);  // ⚠️ Error swallowed
                }
            },
            Err(e) => {
                let _ = callback.call(
                    Err(Error::from(ErrorWrapper::from(e))),
                    ThreadsafeFunctionCallMode::Blocking,
                );  // ⚠️ Error swallowed
            }
        },
        || {},
    );

Issue: Errors in callbacks are swallowed (let _ = ...). If the JavaScript callback throws, the stream continues silently.

Comparison: The stream_all_messages implementation (line 106-127) has better error handling with logging.

Recommendation: Add error logging similar to stream_all_messages:

match callback.call(Ok(msg), ...) {
    Ok(_) => {},
    Err(e) => tracing::error!("Edit stream callback failed: {:?}", e),
}

Code Quality Issues

8. Inconsistent Error Messages 🟢

The test expectations are good, but error messages could be more informative:

  • "Should not be able to edit a deleted message" (test assertion)
  • ✅ Should be: "Cannot edit message: message has been deleted"

This helps with debugging in production.


9. Magic Numbers in Tests 🟢

Location: bindings/mobile/src/mls/tests/content_types.rs:2319

let fake_msg_id = vec![0xDE, 0xAD, 0xBE, 0xEF];

Suggestion: Use a constant or helper function:

const INVALID_MESSAGE_ID: [u8; 4] = [0xDE, 0xAD, 0xBE, 0xEF];

Positive Observations ✅

  1. Excellent Test Coverage - The PR includes comprehensive tests across all scenarios:

    • Basic edit functionality
    • Multiple edits
    • Unauthorized edits
    • Edit-then-delete chains
    • Reply editing with structure preservation
    • Edge cases (deleted messages, reactions, empty content)
  2. Consistent API Design - The editMessage() and stream_message_edits() APIs are consistent across all three binding layers (mobile, node, WASM).

  3. Proper Streaming Implementation - The streaming callbacks properly handle errors and provide cleanup via on_close.

  4. Reply Preservation Logic - The core concept of preserving reply structure when editing is well-thought-out and addresses a real UX need.


Recommendations Summary

Before Merge:

  1. ✅ Verify authorization checks are in core layer (not just bindings)
  2. ✅ Add validation for empty edited content if not present
  3. ✅ Fix error swallowing in Node streaming (add logging)
  4. ✅ Add test for concurrent edits from multiple users

Post-Merge (Nice to Have):
5. Improve error messages for better debugging
6. Consider batching for high-frequency edit streams
7. Add metrics/observability for edit operations


Overall Assessment: This is a solid implementation with good architectural decisions. The main concerns are around test coverage gaps reported by Codecov and ensuring authorization is enforced at the core layer. Once these are addressed, this should be safe to merge.


@mchenani mchenani marked this pull request as ready for review March 17, 2026 22:31
@mchenani mchenani requested a review from a team as a code owner March 17, 2026 22:31
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Mar 17, 2026

Approvability

Verdict: Needs human review

This PR introduces new edit message functionality across all FFI bindings (mobile, node, wasm), including streaming for edits and complex Reply structure preservation logic. While the author owns all changed files and the PR includes comprehensive tests, this adds significant new user-facing capability with non-trivial runtime logic that warrants human review.

No code changes detected at 1f95d4c. Prior analysis still applies.

You can customize Macroscope's approvability policy. Learn more.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 17, 2026

Codecov Report

❌ Patch coverage is 73.11828% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.14%. Comparing base (ae2387b) to head (1f95d4c).

Files with missing lines Patch % Lines
bindings/mobile/src/mls.rs 59.52% 17 Missing ⚠️
bindings/mobile/src/message.rs 84.31% 8 Missing ⚠️
Additional details and impacted files
@@                   Coverage Diff                    @@
##           mc/edit-message-core    #3346      +/-   ##
========================================================
+ Coverage                 83.12%   83.14%   +0.02%     
========================================================
  Files                       378      378              
  Lines                     52034    52124      +90     
========================================================
+ Hits                      43254    43341      +87     
- Misses                     8780     8783       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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