Only this pageAll pages
Powered by GitBook
1 of 4

BlueBubbles Private API

Loading...

Loading...

Loading...

Loading...

Intro and Background

Info on how the Private API bundle was created, what functions it uses, how to contribute, and more.

Background

We initially created the helper bundle with the goal of allowing BlueBubbles clients to be able to see and send typing indicators, as well as to send reactions. Over time, the helper project has grown to support the following:

  1. Send reactions

  2. Send and receive typing indicators

  3. Mark chats read on the server Mac

  4. Mark chats unread on the server Mac (requires MacOS 13+)

  5. Rename group chats

  6. Add / remove participants from group chats

  7. Leave group chat

  8. Update group chat photo (requires MacOS 11+)

  9. Send messages

  10. Send replies (requires MacOS 11+)

  11. Send message effects

  12. Send message with subject

  13. Send mentions

  14. Update pinned chats on the server Mac (requires MacOS 11, higher versions are currently unsupported)

  15. Edit messages (requires MacOS 13+)

  16. Unsend messages (requires MacOS 13+)

  17. Check user focus status (requires MacOS 12+)

  18. Force notify a message (requires MacOS 12+)

  19. Retrieve Digital Touch and Handwritten message previews (requires MacOS 11+)

  20. Create chats

  21. Delete chats

  22. Delete messages

  23. Check user iMessage and FaceTime status

Supported Devices & Platforms

The bundle has been tested on MacOS 10.13 (High Sierra) - MacOS 13 (Ventura). It could work on higher or lower MacOS versions, but we do not know for sure.

The bundle supports both Intel and Apple Silicon Macs.

Installation

How to configure your server to enable the Private API

In order to get Private API features, you must disable MacOS extra security measures, called System Integrity Protection (SIP). The reason for this is because Apple does not let us access the internal iMessage code to do things like send reactions if SIP is enabled. When disabled, we can inject a helper process into the iMessage app to call the internal functions for us. In a way, disabling SIP is similar to Jailbreaking your iPhone.

Disable SIP at your own risk! We are not responsible for any damages, issues, or glitches caused by disabling SIP or by using the Private API.

If you use common sense when downloading and installing things, you should be just fine. Please be careful!

Disabling SIP on an Apple Silicon Mac disables the ability to install and run iOS Apps on your Mac. If this feature is important to you, don't use the Private API.

If you are using a Virtual Machine, please take a snapshot before continuing! You will definitely want a failsafe in case something goes wrong.

Now that the warnings are out of the way, lets proceed to the instructions!

Supported Device Info

The Helper Bundle currently supports macOS 10.13 and up, on both Intel and Apple Silicon Macs.

It may support lower macOS versions, but we haven't been able to test them yet.

Instructions

Please follow the instructions for your macOS version in the tabs below.

Troubleshooting

  • Make sure you have the Private API switch turned on in both the server and the client app

  • Try force quitting and reopening the server (with private API switch toggled on)

  • Run csrutil status inside Terminal, then join our Discord and let us know what the output is.

If none of this works, you should join our Discord and the developers will be able to help you out. In your post, please include your macOS version, Mac chipset (Intel / Apple Silicon), and your server version. Thanks!

IMCore Documentation

Here's what we've found and learned while messing with IMCore

Chats

Getting a Chat Object

Chat objects are defined as IMChat, and we access these via a lookup by guid.

To do this, you'll want to add the IMChatRegistry.h header into your project.

// Retrieve a IMChat instance from a given guid
//
// Uses the chat registry to get an existing instance of a chat based on the chat guid
+(IMChat *) getChat: (NSString *) guid {
    if(guid == nil) return nil;

    IMChat* imChat = [[IMChatRegistry sharedInstance] existingChatWithGUID: guid];
    return imChat;
}

Start/Stop Typing in a Chat

This one is relatively simple:

[chat setLocalUserIsTyping:YES];
[chat setLocalUserIsTyping:NO];

Are we Receiving a Typing Indicator?

This one is also pretty simple:

chat.lastIncomingMessage.isTypingMessage

This will provide a bool value. However, this won't provide you with live updates on typing status. To do this, you'll need to make use of ZKSwizzle (read more about this in Contribution Resources).

Listen to Typing Indicator Status

// Credit to w0lf
// Handles all of the incoming typing events
ZKSwizzleInterface(BBH_IMMessageItem, IMMessageItem, NSObject)
@implementation BBH_IMMessageItem

- (BOOL)isCancelTypingMessage {
    // isCancelTypingMessage seems to also have some timing issues and adding a delay would fix this
    // But I would rather not rely on delays to have this program work properly
    //
    // We would rather that the typing message be cancelled prematurely rather
    // than having the typing indicator stuck permanently
    NSString *guid = [self getGuid];

    if(guid != nil) {

        if([self isLatestMessage]) {
            // handle stopped typing status here
        }
    }
    return ZKOrig(BOOL);
}

- (BOOL)isIncomingTypingMessage {
    // We do this because the isIncomingTypingMessage seems to have some timing
    // issues and will sometimes notify after the isCancelTypingMessage so we need to confirm
    // that the sender actually is typing
    [self updateTypingState];

    // This is here to ensure that no infinite typing occurs
    // If for whatever reason the isCancelTypingMessage does not occur,
    // this should catch the error in 2 seconds
    double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        if(self != nil) {
            NSString *guid = [self getGuid];
            if(guid != nil) {
                if([BlueBubblesHelper isTyping:guid] == NO) {
                    // handle stopped typing status here
                }
            }
        }

    });

    return ZKOrig(BOOL);
}

// Check to see if this IMMessageItem matches the last IMChat's message
// This helps to avoid spamming of the tcp socket
- (BOOL) isLatestMessage {
    NSString *guid = [self getGuid];
    // Fetch the current IMChat to get the IMMessage
    IMChat *chat = [BlueBubblesHelper getChat:guid];
    IMMessageItem *item = (IMMessageItem*) self;
    IMMessage *message = item.message;
    if(message.isFromMe) return NO;

    // If the IMChat's last message matches our own IMMessage, then we can proceed
    // this should avoid spamming of the tcp socket
    return chat.lastIncomingMessage.guid == message.guid;
}

// Update the typing state by checking the message state
- (void) updateTypingState {
    if(![self isLatestMessage]) return;

    NSString *guid = [self getGuid];

    // If we failed to get the guid for whatever reason, then we can't do anything
    if(guid != nil) {
        IMChat *chat = [BlueBubblesHelper getChat:guid];
        // Send out the correct response over the tcp socket
        if(chat.lastIncomingMessage.isTypingMessage == YES) {
            // handle started typing status here
        } else {
            // handle stopped typing status here
        }
    }
}

@end

In the above code, we use swizzling techniques to "intercept" when iMessage calls the isCancelTypingMessage and isIncomingTypingMessage functions. Due to some timing issues described in the code comments, we double verify to make sure the user has actually started or stopped typing, and then pass that status back to clients.

Mark Chat Read

Like the rest, this is fairly straightforward:

[chat markAllMessagesAsRead];

This will remove the unread dot from macOS iMessage.

Mark Chat Unread

Like the rest, this is fairly straightforward:

[chat markLastMessageAsUnread];

This will add an unread dot to the chat in macOS iMessage.

This method requires macOS Ventura (13.0) or above!

Change Group Chat Name

Once again, another easy method:

[chat _setDisplayName:(@"new name here")];

Add or Remove Participants from a Group Chat

NSArray<IMHandle*> *handles = [[IMHandleRegistrar sharedInstance] getIMHandlesForID:(@"some address here")];

// when removing participants, you don't need to do this if block
if (handles != nil) {
    IMAccountController *sharedAccountController = [IMAccountController sharedInstance];
    IMAccount *myAccount = [sharedAccountController mostLoggedInAccount];
    IMHandle *handle = [[IMHandle alloc] initWithAccount:(myAccount) ID:(@"some address here") alreadyCanonical:(YES)];
    handles = @[handle];
}

if([chat canAddParticipants:(handles)]) {
    // to add
    [chat inviteParticipantsToiMessageChat:(handles) reason:(0)];
    // to remove
    [chat removeParticipantsFromiMessageChat:(handles) reason:(0)];
}

First, you'll want to pass addresses to IMHandleRegistrar so you can get IMHandle objects for them.

Then make sure those handles are added to the IMAccount (iMessage account), and after that make sure those handles can be added to the chat itself.

Finally, add the participant (or remove the participant). For whatever reason, the reason argument must be 0 or it doesn't work.

There is one caveat with adding and removing participants with this code. The participant has to be inside the iMessage chat.db, otherwise they may not get added. We haven't been able to find a workaround or a way of registering the participant inside the database as of yet.

Update Chat Pinned Status

// if the chat is pinned, unpin it, otherwise pin it
if (!chat.isPinned) {
    // get the pinned conversation set, make it mutable, and then add the chat to be pinned
    NSArray* arr = [[[IMPinnedConversationsController sharedInstance] pinnedConversationIdentifierSet] array];
    NSMutableArray<NSString*>* chatArr = [[NSMutableArray alloc] initWithArray:(arr)];
    [chatArr addObject:(chat.pinningIdentifier)];
    // convert mutable back to immutable
    NSArray<NSString*>* finalArr = [chatArr copy];
    // update the pinned conversation array
    IMPinnedConversationsController* controller = [IMPinnedConversationsController sharedInstance];
    // contextMenu is an arbitrary value, other values may work as well
    [controller setPinnedConversationIdentifiers:(finalArr) withUpdateReason:(@"contextMenu")];
} else {
    // get the pinned conversation set, make it mutable, and then remove the chat to be unpinned
    NSArray* arr = [[[IMPinnedConversationsController sharedInstance] pinnedConversationIdentifierSet] array];
    NSMutableArray<NSString*>* chatArr = [[NSMutableArray alloc] initWithArray:(arr)];
    [chatArr removeObject:(chat.pinningIdentifier)];
    // convert mutable back to immutable
    NSArray<NSString*>* finalArr = [chatArr copy];
    // update the pinned conversation array
    IMPinnedConversationsController* controller = [IMPinnedConversationsController sharedInstance];
    // contextMenu is an arbitrary value, other values may work as well
    [controller setPinnedConversationIdentifiers:(finalArr) withUpdateReason:(@"contextMenu")];
}

We first grab all the pinned conversations. Then we add or remove the conversation from this array, and send it back to IMPinnedConversationsController. The update reason should be contextMenu otherwise the code may not work.

Pinning chats was introduced on Big Sur, and as such this code will crash if run on macOS 10. This code also seems to crash on Monterey and up - since we cannot run classdump on Monterey yet we have been unable to check and see if the APIs changed.

For now, only use this code on macOS Big Sur.

Deleting a Chat

// Get the chat
IMChat *chat = [BlueBubblesHelper getChat: data[@"chatGuid"] :transaction];
// Use the chat registry to remove the chat
if (chat != nil) {
    [[IMChatRegistry sharedInstance] _chat_remove:(chat)];
}

Pretty simple method. This is not recoverable, the chat and its messages will be permanently deleted!

Messages

Getting an IMMessage

This object is very important when sending a reply or a tapback. We need it to create the association between the existing message that is replied or "tapbacked" to, and the reply or tapback.

+(void) getMessageItem:(IMChat *)chat :(NSString *)actionMessageGuid completionBlock:(void (^)(IMMessage *message))block {
    [[IMChatHistoryController sharedInstance] loadMessageWithGUID:(actionMessageGuid) completionBlock:^(IMMessage *message) {
        block(message);
    }];
}

This is an asynchronous process in IMCore, so it requires the use of a completion block.

Sending a Message (Including Subject, Effect, Mention, and/or Reply)

// now we will deserialize the attributedBody if it exists
NSDictionary *attributedDict = data[@"attributedBody"];
// we'll create the NSMutableAttributedString with the associatedBody string if we can,
// else we'll fall back to using the message text
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString: data[@"message"]];
// if associateBody exists, we iterate through it
if (attributedDict != NULL && attributedDict != (NSDictionary*)[NSNull null]) {
    attributedString = [[NSMutableAttributedString alloc] initWithString: attributedDict[@"string"]];
    NSArray *attrs = attributedDict[@"runs"];
    for(NSDictionary *dict in attrs)
    {
        // construct range and attributes from dict and add to NSMutableAttributedString
        NSArray *rangeArray = dict[@"range"];
        NSRange range = NSMakeRange([(NSNumber*)[rangeArray objectAtIndex:0] intValue], [(NSNumber*)[rangeArray objectAtIndex:1] intValue]);
        NSDictionary *attrsDict = dict[@"attributes"];
        [attributedString addAttributes:attrsDict range:range];
    }
}

NSMutableAttributedString *subjectAttributedString = nil;
if (data[@"subject"] != [NSNull null] && [data[@"subject"] length] != 0) {
    subjectAttributedString = [[NSMutableAttributedString alloc] initWithString: data[@"subject"]];
}
NSString *effectId = nil;
if (data[@"effectId"] != [NSNull null] && [data[@"effectId"] length] != 0) {
    effectId = data[@"effectId"];
}

void (^createMessage)(NSAttributedString*, NSAttributedString*, NSString*, NSString*) = ^(NSAttributedString *message, NSAttributedString *subject, NSString *effectId, NSString *threadIdentifier) {
    IMMessage *messageToSend = [[IMMessage alloc] init];
    messageToSend = [messageToSend initWithSender:(nil) time:(nil) text:(message) messageSubject:(subject) fileTransferGUIDs:(nil) flags:(100005) error:(nil) guid:(nil) subject:(nil) balloonBundleID:(nil) payloadData:(nil) expressiveSendStyleID:(effectId)];
    [chat sendMessage:(messageToSend)];
    if (transaction != nil) {
        [[NetworkController sharedInstance] sendMessage: @{@"transactionId": transaction, @"identifier": [[chat lastMessage] guid]}];
    }
};

createMessage(attributedString, subjectAttributedString, effectId, nil);
// now we will deserialize the attributedBody if it exists
NSDictionary *attributedDict = data[@"attributedBody"];
// we'll create the NSMutableAttributedString with the associatedBody string if we can,
// else we'll fall back to using the message text
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString: data[@"message"]];
// if associateBody exists, we iterate through it
if (attributedDict != NULL && attributedDict != (NSDictionary*)[NSNull null]) {
    attributedString = [[NSMutableAttributedString alloc] initWithString: attributedDict[@"string"]];
    NSArray *attrs = attributedDict[@"runs"];
    for(NSDictionary *dict in attrs)
    {
        // construct range and attributes from dict and add to NSMutableAttributedString
        NSArray *rangeArray = dict[@"range"];
        NSRange range = NSMakeRange([(NSNumber*)[rangeArray objectAtIndex:0] intValue], [(NSNumber*)[rangeArray objectAtIndex:1] intValue]);
        NSDictionary *attrsDict = dict[@"attributes"];
        [attributedString addAttributes:attrsDict range:range];
    }
}
// if we've got a subject, make the string
NSMutableAttributedString *subjectAttributedString = nil;
if (data[@"subject"] != [NSNull null] && [data[@"subject"] length] != 0) {
    subjectAttributedString = [[NSMutableAttributedString alloc] initWithString: data[@"subject"]];
}
// if we've got an effect ID, make the string
NSString *effectId = nil;
if (data[@"effectId"] != [NSNull null] && [data[@"effectId"] length] != 0) {
    effectId = data[@"effectId"];
}
// function we can call to create the message easily
void (^createMessage)(NSAttributedString*, NSAttributedString*, NSString*, NSString*) = ^(NSAttributedString *message, NSAttributedString *subject, NSString *effectId, NSString *threadIdentifier) {
    IMMessage *messageToSend = [[IMMessage alloc] init];
    messageToSend = [messageToSend initWithSender:(nil) time:(nil) text:(message) messageSubject:(subject) fileTransferGUIDs:(nil) flags:(100005) error:(nil) guid:(nil) subject:(nil) balloonBundleID:(nil) payloadData:(nil) expressiveSendStyleID:(effectId)];
    // remove this line of code if not on Big Sur and up!!
    messageToSend.threadIdentifier = threadIdentifier;
    [chat sendMessage:(messageToSend)];
};
// create a thread identifier if we are replying (ONLY WORKS ON BIG SUR)
if (data[@"selectedMessageGuid"] != [NSNull null] && [data[@"selectedMessageGuid"] length] != 0) {
    [BlueBubblesHelper getMessageItem:(chat) :(data[@"selectedMessageGuid"]) completionBlock:^(IMMessage *message) {
        IMMessageItem *messageItem = (IMMessageItem *)message._imMessageItem;
        NSObject *items = messageItem._newChatItems;
        IMMessagePartChatItem *item;
        // sometimes items is an array so we need to account for that
        if ([items isKindOfClass:[NSArray class]]) {
            for(IMMessagePartChatItem* imci in (NSArray *)items) {
                if([imci._item.guid isEqualToString:(data[@"selectedMessageGuid"])]) {
                    item = imci;
                }
            }
        } else {
            item = (IMMessagePartChatItem *)items;
        }
        NSString *identifier = @"";
        // either reply to an existing thread or create a new thread
        if (message.threadIdentifier != nil) {
            identifier = message.threadIdentifier;
        } else {
            identifier = IMCreateThreadIdentifierForMessagePartChatItem(item);
        }
        createMessage(attributedString, subjectAttributedString, effectId, identifier);
    }];
// otherwise just send a regular message
} else {
    createMessage(attributedString, subjectAttributedString, effectId, nil);
}`

There's a lot to break down here. This serves as our all-in-one function of sending a message with an effect, subject line, mention, or reply, or any combination of these things (a message could have all of these things at once).

An example JSON object sent to the bundle might look like this:

{
    "chatGuid": "iMessage;-;<chat address>",
    "subject": "Test", //message subject
    "message": "This is a reply, mention, subject, and effect message", //message text
    "attributedBody": {
        "runs": [{
            "attributes": {
                "__kIMMessagePartAttributeName": 0, //must be 0
                "__kIMMentionConfirmedMention": "<mention contact address>"
            },
            "range": [17, 7] //range is [startIndex, length] not [startIndex, endIndex]
        }],
        "string": "mention" //the string of text to show as the mention
    },
    "effectsId": "com.apple.MobileSMS.expressivesend.impact", //slam effect
    "selectedMessageGuid": "<GUID>" //guid for message being replied to
}

And this is how we parse it:

  1. Deserialize the attributedBody data object. This is what contains mentions data sent from the client, specifically the range of text that is the mention, and what address it is mentioning.

  2. If this data object exists, generate the message NSAttributedString from it. Otherwise, just make a plaintext NSAttributedString.

  3. If we have a subject, then make a subject NSAttributedString (otherwise null)

  4. If we have an effect ID, make an effect ID NSString (otherwise null)

  5. Big Sur+ Only - If we have a selected message GUID (which means the user replied to something), generate a thread originator GUID.

    1. We first have to get the IMMessage object for the selected message

    2. Get an IMMessagePartChatItem from the IMMessage. We have to be very careful here as sometimes the getter returns an array, and sometimes it returns a single object.

    3. Finally, we make the thread identifier. The original message may already have the thread identifier, in which case we use that (i.e. it's already part of a thread). If it doesn't, we can use IMCreateThreadIdentifierForMessagePartChatItem to generate one for us.

  6. Send the message with all the parameters (most can be null). Flags set to 100005 is arbitrary, that's what worked for us in our testing.

Be sure to only use the Big Sur specific methods on Big Sur, otherwise the iMessage app will crash on macOS 10.

Parsing a Tapback into its Tapback ID

+(long long) parseReactionType:(NSString *)reactionType {
    NSString *lowerCaseType = [reactionType lowercaseString];

    if([@"love" isEqualToString:(lowerCaseType)]) return 2000;
    if([@"like" isEqualToString:(lowerCaseType)]) return 2001;
    if([@"dislike" isEqualToString:(lowerCaseType)]) return 2002;
    if([@"laugh" isEqualToString:(lowerCaseType)]) return 2003;
    if([@"emphasize" isEqualToString:(lowerCaseType)]) return 2004;
    if([@"question" isEqualToString:(lowerCaseType)]) return 2005;
    if([@"-love" isEqualToString:(lowerCaseType)]) return 3000;
    if([@"-like" isEqualToString:(lowerCaseType)]) return 3001;
    if([@"-dislike" isEqualToString:(lowerCaseType)]) return 3002;
    if([@"-laugh" isEqualToString:(lowerCaseType)]) return 3003;
    if([@"-emphasize" isEqualToString:(lowerCaseType)]) return 3004;
    if([@"-question" isEqualToString:(lowerCaseType)]) return 3005;
    return 0;
}

This function simply converts text based reaction types to their integer counterparts in iMessage.

Sending a Tapback

//Map the reaction type
long long reactionLong = [BlueBubblesHelper parseReactionType:(data[@"reactionType"])];
// Get the messageItem
[BlueBubblesHelper getMessageItem:(chat) :(data[@"selectedMessageGuid"]) completionBlock:^(IMMessage *message) {
    IMMessageItem *imMessage = (IMMessageItem *)message._imMessageItem;
    NSObject *items = imMessage._newChatItems;
    IMChatItem *item;
    // sometimes items is an array so we need to account for that
    if ([items isKindOfClass:[NSArray class]]) {
        for(IMChatItem* imci in (NSArray *)items) {
            if([imci._item.guid isEqualToString:(data[@"selectedMessageGuid"])]) {
                item = imci;
            }
        }
    } else {
        item = (IMChatItem *)items;
    }
    //Build the message summary
    NSDictionary *messageSummary = @{@"amc":@1,@"ams":[imMessage body].string};
    // Send the tapback
    // check if the body happens to be an object (ie an attachment) and send the tapback accordingly to show the proper summary
    NSData *dataenc = [[imMessage body].string dataUsingEncoding:NSNonLossyASCIIStringEncoding];
    NSString *encodevalue = [[NSString alloc]initWithData:dataenc encoding:NSUTF8StringEncoding];
    if ([encodevalue isEqualToString:@"\\ufffc"]) {
        [chat sendMessageAcknowledgment:(reactionLong) forChatItem:(item) withMessageSummaryInfo:(@{})];
    } else {
        [chat sendMessageAcknowledgment:(reactionLong) forChatItem:(item) withMessageSummaryInfo:(messageSummary)];
    }
}];
if (attributedString == nil) {
    NSString *message = data[@"message"];
    // Tapbacks will not have message text, but messages sent must have some sort of text
    if (message == nil) {
        message = @"TEMP";
    }
    attributedString = [[NSMutableAttributedString alloc] initWithString: message];
}

void (^createMessage)(NSAttributedString*, NSAttributedString*, NSString*, NSString*, NSString*, long long*, NSRange, NSDictionary*, NSArray*, BOOL) = ^(NSAttributedString *message, NSAttributedString *subject, NSString *effectId, NSString *threadIdentifier, NSString *associatedMessageGuid, long long *reaction, NSRange range, NSDictionary *summaryInfo, NSArray *transferGUIDs, BOOL isAudioMessage) {
    messageToSend = [messageToSend initWithSender:(nil) time:(nil) text:(message) messageSubject:(subject) fileTransferGUIDs:(nil) flags:(5) error:(nil) guid:(nil) subject:(nil) associatedMessageGUID:(associatedMessageGuid) associatedMessageType:*(reaction) associatedMessageRange:(range) messageSummaryInfo:(summaryInfo)];
    [chat sendMessage:(messageToSend)];
};

[BlueBubblesHelper getMessageItem:(chat) :(data[@"selectedMessageGuid"]) completionBlock:^(IMMessage *message) {
    IMMessageItem *messageItem = (IMMessageItem *)message._imMessageItem;
    NSObject *items = messageItem._newChatItems;
    IMMessagePartChatItem *item;
    // sometimes items is an array so we need to account for that
    if ([items isKindOfClass:[NSArray class]]) {
        for (IMMessagePartChatItem *i in (NSArray *) items) {
            if ([i index] == [data[@"partIndex"] integerValue]) {
                item = i;
                break;
            }
        }
    } else {
        item = (IMMessagePartChatItem *)items;
    }
    NSString *reaction = data[@"reactionType"];
    long long reactionLong = [BlueBubblesHelper parseReactionType:(reaction)];
    NSDictionary *messageSummary;
    // if we actually got an item, proceed, otherwise use a fallback
    if (item != nil) {
        NSAttributedString *text = [item text];
        if (text == nil) {
            text = [message text];
        }
        messageSummary = @{@"amc":@1,@"ams":text.string};
        // Send the tapback
        // check if the body happens to be an object (ie an attachment) and send the tapback accordingly to show the proper summary
        NSData *dataenc = [text.string dataUsingEncoding:NSNonLossyASCIIStringEncoding];
        NSString *encodevalue = [[NSString alloc]initWithData:dataenc encoding:NSUTF8StringEncoding];
        if ([encodevalue isEqualToString:@"\\ufffc"]) {
            NSMutableAttributedString *newAttributedString = [[NSMutableAttributedString alloc] initWithString: [[BlueBubblesHelper reactionToVerb:(reaction)] stringByAppendingString:(@"an attachment")]];
            createMessage(newAttributedString, subjectAttributedString, effectId, nil, [NSString stringWithFormat:@"p:%@/%@", data[@"partIndex"], [message guid]], &reactionLong, [item messagePartRange], @{}, nil, false);
        } else {
            NSMutableAttributedString *newAttributedString = [[NSMutableAttributedString alloc] initWithString: [[BlueBubblesHelper reactionToVerb:(reaction)] stringByAppendingString:([NSString stringWithFormat:(@"“%@”"), text.string])]];
            if ([item text] == nil) {
                createMessage(newAttributedString, subjectAttributedString, effectId, nil, [NSString stringWithFormat:@"bp:%@", [message guid]], &reactionLong, [item messagePartRange], messageSummary, nil, false);
            } else {
                createMessage(newAttributedString, subjectAttributedString, effectId, nil, [NSString stringWithFormat:@"p:%@/%@", data[@"partIndex"], [message guid]], &reactionLong, [item messagePartRange], messageSummary, nil, false);
            }
        }
    } else {
        messageSummary = @{@"amc":@1,@"ams":message.text.string};
        // Send the tapback
        // check if the body happens to be an object (ie an attachment) and send the tapback accordingly to show the proper summary
        NSData *dataenc = [[message text].string dataUsingEncoding:NSNonLossyASCIIStringEncoding];
        NSString *encodevalue = [[NSString alloc]initWithData:dataenc encoding:NSUTF8StringEncoding];
        NSRange range = NSMakeRange(0, [message text].string.length);
        if ([encodevalue isEqualToString:@"\\ufffc"] || [encodevalue length] == 0) {
            NSMutableAttributedString *newAttributedString = [[NSMutableAttributedString alloc] initWithString: [[BlueBubblesHelper reactionToVerb:(reaction)] stringByAppendingString:(@"an attachment")]];
            createMessage(newAttributedString, subjectAttributedString, effectId, nil, [message guid], &reactionLong, range, @{}, nil, false);
        } else {
            NSMutableAttributedString *newAttributedString = [[NSMutableAttributedString alloc] initWithString: [[BlueBubblesHelper reactionToVerb:(reaction)] stringByAppendingString:([NSString stringWithFormat:(@"“%@”"), [message text].string])]];
            createMessage(newAttributedString, subjectAttributedString, effectId, nil, [message guid], &reactionLong, range, messageSummary, nil, false);
        }
    }
}];
  1. Get the integer representation of the tapback using the parseReactionType function.

  2. Get the IMMessage object for the message the user selected to react to

  3. Get an IMMessagePartChatItem from this IMMessage. As is the case when sending a reply, sometimes the getter is an array and sometimes it is the single object, so watch out.

  4. Build a message summary. This is what is shown on the chat list page, e.g. 'John liked "Test"'. amc stands for associated message content, while ams stands for associated message summary.

  5. Set the second value to the body of the message being reacted to. If this message is a non-text message, the body string will be the unicode ufffc.

  6. Due to the above, we want to make sure we don't mistakenly send a summary with a non-displayable unicode character inside it. Thus, we set the summary to null if we detect the body string to be ufffc.

  7. MacOS 11+ requires some extra things:

    1. Get the range for the message being reacted to - this refers directly with the IMMessagePartChatItem as a message can have multiple items and the range helps identify which one

    2. Add the partIndex to the associated message guid: p:<index>/<guid>

    3. Provide a fake message NSAttributedString - this is not actually used anywhere but avoids crashes

Editing a Message

// get the chat
IMChat *chat = [BlueBubblesHelper getChat: data[@"chatGuid"] :transaction];
// get the message
[BlueBubblesHelper getMessageItem:(chat) :(data[@"messageGuid"]) completionBlock:^(IMMessage *message) {
    // generate the two NSMutableAttributedStrings
    NSMutableAttributedString *editedString = [[NSMutableAttributedString alloc] initWithString: data[@"editedMessage"]];
    NSMutableAttributedString *bcString = [[NSMutableAttributedString alloc] initWithString: data[@"backwardsCompatibilityMessage"]];
    NSInteger index = data["@partIndex"];
    // send the edit
    [chat editMessage:(message) atPartIndex:(index) withNewPartText:(editedString) backwardCompatabilityText:(bcString)];
}];

This one is fairly easy. The only tricky part is the part index, which corresponds to which part of a message is being edited. Sometimes, a message can be stacked as so:

Message

Attachment

Message

The part index tells IMCore which part of the message is actually being edited, as there are two separate message strings that could be changed. The indexes start at 0.

Unsending a Message

// get the chat
IMChat *chat = [BlueBubblesHelper getChat: data[@"chatGuid"] :transaction];
// get the message
[BlueBubblesHelper getMessageItem:(chat) :(data[@"messageGuid"]) completionBlock:^(IMMessage *message) {
    // find the message item corresponding to the part index
    IMMessageItem *messageItem = (IMMessageItem *)message._imMessageItem;
    NSObject *items = messageItem._newChatItems;
    IMMessagePartChatItem *item;
    // sometimes items is an array so we need to account for that
    if ([items isKindOfClass:[NSArray class]]) {
        for (IMMessagePartChatItem *i in (NSArray *) items) {
            if ([i index] == [data[@"partIndex"] integerValue]) {
                item = i;
                break;
            }
        }
    } else {
        item = (IMMessagePartChatItem *)items;
    }
    // retract (unsend) the message
    [chat retractMessagePart:(item)];
}];

As with editing a message, the part index is crucial to know which part has been unsent and then use the IMMessagePartChatItem in the method correctly.

Currently Known Issues

Building and Contributing

Here's how you can build or contribute to the project

Build Instructions

Pre-Requisites

  1. macOS device with Xcode and working iMessage

  2. Apple Developer account with valid Team ID

You do not need to pay the $100 fee for the Apple Developer account. As soon as your account is approved after the initial creation, you should be able to access your Team ID from within Xcode.

Instructions

  1. Open the MacOS-xx folder (where xx corresponds with your current macOS version) within the cloned repository files

    • i.e. Messages/MacOS-11+

    • i.e. Messages/MacOS-10

    • ie. FaceTime/MacOS-11+

  2. Open up Terminal and navigate to the same directory

  3. Run pod install

    • This should install the the dependencies for the project

  4. Using finder, double click BlueBubblesHelper.xcworkspace to open inside Xcode

    • Make sure you do not open the BlueBubblesHelper.xcodeproj file

  5. Select the BlueBubblesHelper Dylib target (building icon) in the secondary sidebar. Then go to the Build Phases tab and expand the Copy Files section. Edit the Path to be where you want the dylib to output to. It's recommended that you set the path to be in the appResources of your BlueBubbles Server source code, for instance: /{path_to_your_code}/packages/server/appResources/private-api/macos11

  6. Select the Signing & Capabilities tab and sign in (or select) your Developer account

  7. Ensure that you have the BlueBubblesHelper Dylib selected as the build target

  8. Now, you are ready to build. Just hit the play button, and the dylib will be built, outputting to the proper location within the BlueBubbles Server source code (appResources). The Messages app will be killed, which will prompt the BlueBubbles Server to restart the Messages app with the new dylib build.

Common Issues

ld: library 'CocoaAsyncSocket' not found clang: error: linker command failed with exit code 1

This means you've done either of the following:

  • You have not run pod install within the MacOS-XX folder of the cloned repository

  • You opened the BlueBubblesHelper.xcodeproj file instead of the BlueBubblesHelper.xcworkspace file

PhaseScriptExecution failed with a nonzero exit code

This error means that the Build Phase -> Run Script configuration for your target errored out.

This is likely due to the killall Messages command failing (throwing an error) because the Messages app is not running.

To fix it, change the script to be killall -q Messages which should allow it to return 0 for the exit code. If that doesn't work, try killall -q Messages; exit 0;

Contributing

The Helper Bundle is quite challenging to get a hang of. The reason for this is because none of the internal iMessage Framework methods are commented or documented in any way. The only reasonable method of development is to make educated guesses and brute-force until you find something that works.

As a result, we highly recommend that you have a working knowledge of Objective-C and/or reverse-engineering/tweak development before attempting to work on the bundle.

Terms / Words to Know

The Helper Bundle development is filled with some jargon that might appear confusing at first. Here are some commonly used terms and what they refer to.

  • IMCore - The iMessage internal framework on macOS. This can be found at /System/Library/Private Frameworks/IMCore.framework, but it requires a classdump tool to actually view the header files inside the framework.

  • IMSharedUtilities - A utility framework for iMessage. This is also found in the same Private Frameworks directory.

  • Console - This refers to the macOS Console app, which is our main tool for debugging the bundle. You will notice that BlueBubblesHelper.m has quite a few DLog statements. These print logs to the system.log tab of the macOS Console on newer macOS versions, while older macOS versions will print these logs to the standard Console tab.

  • classdump - This is the process of dumping header files on macOS. As mentioned above, we use this tool to view the header file code which is otherwise hidden from users.

Contribution Guide

  • Write clean, readable code. Make sure to add comments for things that might seem out of place or strange at first glance.

  • Do not edit any .h files without consulting a main developer. Most of your changes will only happen in the BlueBubblesHelper.m file, as its the one that calls the IMCore header functions.

  • Testing is key. There are so many variables when it comes to adding new functionality. Make sure your code is safe, typed, and works in a variety of different cases.

Contribution Resources

  1. Currently there is no classdump tool that we know of which works on macOS Monterey. However, all Big Sur header files should work fine on Monterey machines.

Enable Private Logs

Often times, you may try to log some data out to the Console App, however, the log shows as <private>. This is because your Mac's Enable-Private-Data setting defaults to false. In order to turn it on to allow private logging, please install the following MacOS Profile:

Once downloaded, just double-click the profile to install it. However, since the profile no longer has a valid signature, you will need to open your System Preferences app, then search Profiles and manually install the profile.

Development Process

The following will attempt to outline how our development process runs from start to finish when adding a new feature to the bundle.

  1. Is the feature reasonable to implement and can we get it to work cross-platform with all clients? Will we be able to send the required data to the bundle from the server app?

    This step is important to ask yourself. Ultimately the bundle is meant to be a companion to the server and clients, since it can't be used as a standalone app. We have to make sure the feature is reasonable in scope and can actually be implemented on clients.

    Consider the limitations that the bundle has, especially in communication with the server. We can only send and receive primitive types from both ends, so data must be serialized and sent.

  2. Now for the hard part - finding out which methods and what arguments we need to generate so iMessage does what we want. This step takes the longest by far, and we recommend using the above resources to speed things up a bit.

    1. Sometimes, you may need to add header files to the Bundle, because the functions you need are in a new file that we haven't added yet. To do this, create a new .h file in the same directory as the others, with the same filename as the file in the IMCore or IMSharedUtilities frameworks.

    2. If you created files in step 1, make sure to import them inside BlueBubblesHelper.m.

    3. At the bottom of the initializeNetworkConnection function inside BlueBubblesHelper.m, we have some commented out lines of code. These are what we use when developing new integrations. Essentially, the handleMessage function is called with "fake data" that we provide to simulate a server-side message. This code will be called around 5 seconds after the bundle is installed, as soon as it has made a connection with the server app, and is the best way to automate your testing process.

    4. Keep testing until you've got code that works!

  3. By this point, you should have working code for your feature. Congrats! The next step is to make sure your code is clean, safe from exceptions, typed properly with correct arguments, and can send correctly-formatted data back to the server, when applicable. We encourage that you comment your code as well.

  4. You probably only developed the integration inside the folder for your macOS version, whether that be 10 or 11+. If the iMessage feature is not version-specific, we also need to add your code to the other folder's BlueBubblesHelper.m file. You have 2 options:

    1. Reach out to a main developer (Tanay has a Catalina and Monterey VM on his Big Sur Mac), and they can add / test your code on the other macOS folder.

  5. Make a PR :)

Exceptions in the bundle are very, very bad. As you might find in your testing, exceptions will crash the entire bundle and iMessage process, which breaks Private API until iMessage is restarted. As a result, it is extremely important that your code is as safe as possible - check for null, check for types, etc.

If you get an exception while developing, take a look at the report that's generated. The line numbers/stacktrace won't be helpful, but the actual error message may help you find the source of the problem.

Disabling SIP can be challenging, we recommend joining our to get assistance with the process if you need it!

Please ensure you have the latest BlueBubbles Server from !

  1. Open Terminal on your macOS device

  2. Run the following command to disable Library Validation and enter your password when prompted.

    sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation.plist DisableLibraryValidation -bool true

  3. Run the following commands to force your Mac to reboot into recovery mode:

    • WARNING: DO NOT do this step or step 4-5 if you are running dos1dude's patched High Sierra/Mojave/Catalina. It may temporarily brick your Mac until you reset your NVRAM/PRAM

    • If you are on a real Mac with official macOS, do the following:

      1. WARNING: This will instantly reboot your Mac. Save everything before executing this command!

      2. sudo nvram recovery-boot-mode=unused

      3. sudo reboot recovery

    • If you are using a Virtual Machine or a patched macOS software (Open Core), follow

  4. When you are booted into Recovery Mode:

    • Click on Utilities in the top menu bar

    • Select Terminal

    • Type this command and hit enter to disable SIP: csrutil disable

  5. Restart your macOS device/server

    • Click the Apple logo in the top menu, then click Restart

  6. Turn the Private API switch on in the BlueBubbles Server settings

  7. Verify that the Private API is connected by clicking the refresh button inside the Private API Status box in BlueBubbles Server settings

  8. Go to Settings > Private API Features on the clients you use, and toggle that on. You should now have functioning Private API Features!

  1. Open Terminal on your macOS device

  2. Run the following command to disable Library Validation and enter your password when prompted.

    sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation.plist DisableLibraryValidation -bool true

  3. Follow the instructions below for your device type

    • Physical Mac, INTEL, official software

      1. Run the following commands to force your Mac to reboot into recovery mode:

        • WARNING: This will instantly reboot your Mac. Save everything before executing these commands!

        • sudo nvram internet-recovery-mode=RecoveryModeDisk

        • sudo reboot recovery

      2. When you are booted into Recovery Mode:

        • Click on Utilities in the top menu bar

        • Select Terminal

        • Type this command and hit enter to disable SIP: csrutil disable

      3. Click the Apple logo in the top menu bar, then click Restart

    • Physical Mac, APPLE SILICON, official software

      1. Do the following to force your Mac to reboot into recovery mode:

        • Shut down the Mac normally

        • Press and hold the power button on your Mac until you see "Loading startup options."

        • Click Options, then click Continue, and enter the admin password if requested.

      2. When you are booted into Recovery Mode:

        • Click on Utilities in the top menu bar

        • Select Terminal

        • Type this command and hit enter to disable SIP: csrutil disable

      3. Click the Apple logo in the top menu bar, then click Restart

    • macOS on a Virtual Machine or patched macOS software on a Physical Mac

  4. Turn the Private API switch on inside the BlueBubbles Server settings

  5. Verify that the Private API is connected by clicking the refresh button inside the Private API Status box in BlueBubbles Server settings

  6. Go to Settings > Private API Features on the clients you use, and toggle that on. You should now have functioning Private API Features!

Here are some basic troubleshooting steps. Please try these out, and if you need more help, feel free to join our !

Sometimes, the IMMessagePartChatItem will not exist. This is almost always for messages that are old. If this happens, tapbacks won't work, and replies will have a weird bug where they attach to an "empty" message. See this for more details.

The reaction function currently doesn't support reacting to a message with multiple parts, for example a message with multiple attachments. This is likely also to do with IMMessagePartChatItem but we haven't figured it out quite yet. See this for more details.

Complete the instructions (you don't need to install the stable bundle file, since Xcode will overwrite it with your built version)

Clone the to your macOS device

Select the BlueBubblesHelper project header in the primary side bar

Select the BlueBubblesHelper target (blue block icon) in the secondary sidebar

Hey there, and welcome! We are always eager to have new contributors working on our projects. First and foremost, we recommend joining our as that is the best place you can get in touch with the main developers to discuss new features, ideas, or changes you're planning to make. Now, on to the guide...

Barcelona - This is Eric Rabil's wonderful REST API for iMessage built entirely on IMCore. Much of our own code has come from looking at and borrowing bits and pieces to make our integrations work.

- if you are new to reverse engineering, this may be a helpful resource.

- pre-dumped headers for macOS Big Sur and up only. If you are developing for Catalina and under, you will need to dump headers on your own machine, using the next tool.

- macOS 10 only, - macOS 11 only.

- open-source tool from Apple that also dumps headers on macOS 11. This has not been tested by any developers as of yet, but may be a good alternative to freedomtan's classdump-dyld fork.

- this tool is already built into the bundle. You can use it to "intercept" Objective-C method calls and see what arguments are passed to them.

Make a VM on your macOS device following the guide for the OS you need: or . We highly recommend using a burner Apple ID on these VMs just in case Apple has a bad mood when you make the VM.

Follow

Discord
GitHub Releases
this guide.
this guide.
Discord
ticket
ticket
Installation
repo
Discord
his repo
Tweak Development Wiki
w0lfschild macOS Headers
Steve Nygard class-dump
freedomtan classdump-dyld
dyld_shared_cache_util
ZKSwizzle
Big Sur
Catalina
LogoShow private log messages in Console.appgrgarside