Loading...
Loading...
Loading...
Loading...
Here's how you can build or contribute to the project
macOS device with Xcode and working iMessage
Apple Developer account with valid Team ID
Complete the Installation instructions (you don't need to install the stable bundle file, since Xcode will overwrite it with your built version)
Clone the repo to your macOS device
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.
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+
Open up Terminal
and navigate to the same directory
Run pod install
This should install the the dependencies for the project
Using finder, double click BlueBubblesHelper.xcworkspace
to open inside Xcode
Make sure you do not open the BlueBubblesHelper.xcodeproj
file
Select the BlueBubblesHelper
project header in the primary side bar
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
Select the BlueBubblesHelper
target (blue block icon) in the secondary sidebar
Select the Signing & Capabilities
tab and sign in (or select) your Developer account
Ensure that you have the BlueBubblesHelper Dylib
selected as the build target
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.
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;
Hey there, and welcome! We are always eager to have new contributors working on our projects. First and foremost, we recommend joining our Discord 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...
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.
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.
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 his repo and borrowing bits and pieces to make our integrations work.
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.
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.
Tweak Development Wiki - if you are new to reverse engineering, this may be a helpful resource.
w0lfschild macOS Headers - 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.
Steve Nygard class-dump - macOS 10 only, freedomtan classdump-dyld - macOS 11 only.
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.
dyld_shared_cache_util - 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.
ZKSwizzle - 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.
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.
The following will attempt to outline how our development process runs from start to finish when adding a new feature to the bundle.
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.
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.
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.
If you created files in step 1, make sure to import them inside BlueBubblesHelper.m
.
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.
Keep testing until you've got code that works!
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.
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:
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.
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.
Info on how the Private API bundle was created, what functions it uses, how to contribute, and more.
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:
Send reactions
Send and receive typing indicators
Mark chats read on the server Mac
Mark chats unread on the server Mac (requires MacOS 13+)
Rename group chats
Add / remove participants from group chats
Leave group chat
Update group chat photo (requires MacOS 11+)
Send messages
Send replies (requires MacOS 11+)
Send message effects
Send message with subject
Send mentions
Update pinned chats on the server Mac (requires MacOS 11, higher versions are currently unsupported)
Edit messages (requires MacOS 13+)
Unsend messages (requires MacOS 13+)
Check user focus status (requires MacOS 12+)
Force notify a message (requires MacOS 12+)
Retrieve Digital Touch and Handwritten message previews (requires MacOS 11+)
Create chats
Delete chats
Delete messages
Check user iMessage and FaceTime status
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.
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.
Disabling SIP can be challenging, we recommend joining our Discord to get assistance with the process if you need it!
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.
Please ensure you have the latest BlueBubbles Server from GitHub Releases!
Now that the warnings are out of the way, lets proceed to the instructions!
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.
Please follow the instructions for your macOS version in the tabs below.
Open Terminal on your macOS device
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
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:
WARNING: This will instantly reboot your Mac. Save everything before executing this command!
sudo nvram recovery-boot-mode=unused
sudo reboot recovery
If you are using a Virtual Machine or a patched macOS software (Open Core), follow this guide.
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
Restart your macOS device/server
Click the Apple logo in the top menu, then click Restart
Turn the Private API switch on in the BlueBubbles Server settings
Verify that the Private API is connected by clicking the refresh button inside the Private API Status box in BlueBubbles Server settings
Go to Settings > Private API Features on the clients you use, and toggle that on. You should now have functioning Private API Features!
Open Terminal on your macOS device
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
Follow the instructions below for your device type
Physical Mac, INTEL, official software
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
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
Click the Apple logo in the top menu bar, then click Restart
Physical Mac, APPLE SILICON, official software
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.
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
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
Follow this guide.
Turn the Private API switch on inside the BlueBubbles Server settings
Verify that the Private API is connected by clicking the refresh button inside the Private API Status box in BlueBubbles Server settings
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 Discord!
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!
Here's what we've found and learned while messing with IMCore
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.
This one is relatively simple:
This one is also pretty simple:
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).
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.
Like the rest, this is fairly straightforward:
This will remove the unread dot from macOS iMessage.
Like the rest, this is fairly straightforward:
This will add an unread dot to the chat in macOS iMessage.
This method requires macOS Ventura (13.0) or above!
Once again, another easy method:
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.
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.
Pretty simple method. This is not recoverable, the chat and its messages will be permanently deleted!
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.
This is an asynchronous process in IMCore, so it requires the use of a completion block.
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:
And this is how we parse it:
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.
If this data object exists, generate the message NSAttributedString
from it. Otherwise, just make a plaintext NSAttributedString
.
If we have a subject, then make a subject NSAttributedString
(otherwise null)
If we have an effect ID, make an effect ID NSString
(otherwise null)
Big Sur+ Only - If we have a selected message GUID (which means the user replied to something), generate a thread originator GUID.
We first have to get the IMMessage
object for the selected message
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.
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.
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.
This function simply converts text based reaction types to their integer counterparts in iMessage.
Get the integer representation of the tapback using the parseReactionType
function.
Get the IMMessage
object for the message the user selected to react to
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.
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.
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
.
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
.
MacOS 11+ requires some extra things:
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
Add the partIndex to the associated message guid: p:<index>/<guid>
Provide a fake message NSAttributedString
- this is not actually used anywhere but avoids crashes
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.
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.
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 ticket 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 ticket for more details.