Skip to content

Fix delete-message handler: call deleteIMMessageItems: instead of deleteChatItems: - #55

Draft
ardydavari wants to merge 1 commit into
BlueBubblesApp:masterfrom
ardydavari:fix/delete-message-calls-wrong-imcore-method
Draft

Fix delete-message handler: call deleteIMMessageItems: instead of deleteChatItems:#55
ardydavari wants to merge 1 commit into
BlueBubblesApp:masterfrom
ardydavari:fix/delete-message-calls-wrong-imcore-method

Conversation

@ardydavari

Copy link
Copy Markdown

Summary

The delete-message action in BlueBubblesHelper.m calls -[IMChat deleteChatItems:] with an array of IMMessagePartChatItem (message parts — text ranges, attachment slots). This hits IMCore's "delete subset of chat items from message → update message" code path, which logs activity that looks like a delete is happening:

(IMCore) [ChatItems] Processing 1 chat items for deletion
(IMCore) [ChatItems] Request to delete subset of chatItems from message. Update message.
(IMCore) [ChatItems] DELETING PARTS index:range map: <private>

…but the message row stays in chat_message_join, no entry is written to sync_deleted_messages, and nothing propagates via iCloud. Meanwhile, the HTTP API returns {\"status\":200,\"message\":\"Successfully deleted message!\"} because the transaction ACK in the helper fires outside the getMessageItem:completionBlock: callback, so the server never sees the no-op.

The correct method for whole-message delete is -[IMChat deleteIMMessageItems:], which takes an array of IMMessageItem and invokes IMCore's real delete path — row removal, sync_deleted_messages entry with a CloudKit recordID, and iCloud propagation to all signed-in devices.

Both methods are already declared in the existing IMChat.h headers for MacOS-10 and MacOS-11+, so no new imports are required.

The fix

 } else if ([event isEqualToString:@\"delete-message\"]) {
     IMChat *chat = [BlueBubblesHelper getChat: data[@\"chatGuid\"] :transaction];
     if (chat != nil) {
         [BlueBubblesHelper getMessageItem:(chat) :(data[@\"messageGuid\"]) completionBlock:^(IMMessage *message) {
             IMMessageItem *messageItem = (IMMessageItem *)message._imMessageItem;
-            NSObject *items = messageItem._newChatItems;
-            IMMessagePartChatItem *item;
-            if ([items isKindOfClass:[NSArray class]]) {
-                [chat deleteChatItems:(items)];
-            } else {
-                [chat deleteChatItems:(@[items])];
+            if (messageItem != nil) {
+                [chat deleteIMMessageItems:@[messageItem]];
             }
         }];
         ...
     }
 }

Applied to both Messages/MacOS-10/BlueBubblesHelper/BlueBubblesHelper.m and Messages/MacOS-11+/BlueBubblesHelper/BlueBubblesHelper.m since both copies had identical incorrect code.

Diagnostic notes

Caught with `log stream --predicate 'process == "Messages"' --level debug` on macOS 26.4 Tahoe while firing DELETE /api/v1/chat/:guid/:messageGuid. The IMCore logs above made it clear the call was going down the part-delete path; cross-referencing IMChat.h in this repo showed deleteIMMessageItems: sitting right next to deleteChatItems: at lines 376-377, confirming the correct method exists.

One subtlety that masks the bug: the helper sends the transaction ACK immediately after kicking off getMessageItem:completionBlock:, not from inside the completion block. So even if the IMCore call was silently throwing or hanging, the server would still see success. Worth being aware of when debugging other actions.

Verification

Verified end-to-end on macOS 26.4 Tahoe (Messages.app 26.0, IMCore 800.0.0):

  1. Rebuilt the MacOS-11+ helper dylib with this fix using clang -dynamiclib directly against the SDK's private frameworks (no Xcode required, just Command Line Tools).
  2. Deployed to /Applications/BlueBubbles.app/Contents/Resources/appResources/private-api/macos11/BlueBubblesHelper.dylib.
  3. Killed and relaunched BlueBubbles. Confirmed the new dylib was injected into Messages.app via lsof -p <messages_pid> | grep BlueBubbles.
  4. Issued 95 delete requests across 1:1 and group chats via a custom script. For each:
    • message row was removed from the table on the issuing device
    • sync_deleted_messages had an entry with a valid CloudKit recordID
    • The delete propagated to the other signed-in Mac's chat_recoverable_message_join within seconds
  5. Zero crashes. Zero verify failures. Zero divergence between devices.

Not verified

  • Behavior on older macOS versions (Big Sur, Monterey, Ventura, Sonoma, Sequoia). The deleteIMMessageItems: selector is present in this repo's class-dumped IMChat.h for both MacOS-10 and MacOS-11+, so it should be available, but I haven't tested outside Tahoe. A quick sanity check from a maintainer on an earlier macOS would be helpful before merging.
  • The send-multipart edge case where _newChatItems might have been relevant. I replaced the _newChatItems access entirely, so if there's a scenario where the helper legitimately needs to delete a subset of parts (e.g., removing an attachment while keeping text), this fix is too aggressive. I couldn't find such a call site in the BlueBubbles server, but if one exists, the fix may need to gate on whether the caller wants whole-message or part-level delete.

Opening as a draft so maintainers can weigh in on macOS-version coverage before landing.

…teChatItems:

The delete-message action was calling -[IMChat deleteChatItems:] with an
array of IMMessagePartChatItem (the message's parts — text ranges, attachment
slots). On macOS 26 Tahoe this invokes IMCore's "delete subset of chat items
from message → update message" path, which logs:

    (IMCore) [ChatItems] Processing 1 chat items for deletion
    (IMCore) [ChatItems] Request to delete subset of chatItems from message.
                         Update message.
    (IMCore) [ChatItems] DELETING PARTS index:range map: <private>

…but the message row stays in chat_message_join and no entry is written to
sync_deleted_messages, so the delete never propagates to other devices via
iCloud. The HTTP API returns {"status":200,"message":"Successfully deleted
message!"} because the transaction ACK fires outside the completion block,
masking the silent no-op.

The correct method is -[IMChat deleteIMMessageItems:], which takes an array
of IMMessageItem (whole messages) and hits the real delete path: the row is
removed from chat_message_join, an entry is added to sync_deleted_messages
with a CloudKit recordID, and the delete propagates across all devices
signed into Messages-in-iCloud.

Both methods are declared in the existing IMChat.h headers for MacOS-10 and
MacOS-11+, so no new imports are needed.

Verified end-to-end on macOS 26.4 Tahoe by:
  1. Rebuilding the MacOS-11+ helper dylib with this fix.
  2. Deploying to /Applications/BlueBubbles.app/.../BlueBubblesHelper.dylib.
  3. Deleting 95 messages from 1:1 and group chats via apply.py / bulk curl.
  4. Confirming each message was removed from message and
     chat_message_join on the issuing device, added to sync_deleted_messages
     with a valid CloudKit recordID, and propagated to
     chat_recoverable_message_join on the user's main Mac via iCloud.

The same fix is applied to both MacOS-10 and MacOS-11+ copies of the
helper source since both have identical incorrect code.
@tneotia

tneotia commented May 6, 2026

Copy link
Copy Markdown
Member

macOS version coverage should be fine if the API is present in both version header dumbs. The second bullet which you mentioned - we would want to check what the native iMessage behavior is. Does the iMessage app support deleting specific parts of a "multipart" message? E.g. receiving multiple attachments + text in a single message, can you delete out a single attachment? I would presume so, but haven't had the chance to check.

If so, we need to find a better way to do this.

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.

2 participants