What?
I’ve been working on an XMPP-based notification and chat hub. My goal is to receive notifications for everything in one place - emails, work ticket updates, script results, meeting reminders, and more - and also be able to chat with others from the same place. This is a work in progress, and slowly but surely I’ve been moving toward this goal. I’ll be writing a post about this system soon. In the meantime, I’d like to share one of the trickier parts of this project: Apple iMessages.
What makes iMessages more difficult is it’s a closed system: There’s no API, you can only send and receive messages from Apple devices, and I couldn’t get any existing solutions to work for me.
I’ve been trying to do this for more than a year, and yesterday I finally got it working!
My approach is a little wacky and convoluted like a Rube Goldberg machine, it’s very rough around the edges, requires a bunch of setup, three programming languages, and involves many moving parts… but it works!!
I hope to tidy things up so this system can be shared and used by others. Until then, I’ll describe the system as it works currently. I’m sure there are better, more efficient ways to do this. Here’s how I did it.
Details
Setup
Aside from the various scripts, I also created an XMPP account for each person with whom I want to exchange iMessages. This is a sort of “puppet bridge,” where the XMPP accounts act as a bridge to iMessage.
I could have set this up where all incoming messages are just routes to my assistant chatbot, but this way I can communicate with iMessage users as if they were XMPP users.
The key part of this system is a working Mac computer logged in to and running iMessages. This is not ideal! An alternative might be a virtualized Mac system. For now I’m using an actual Mac laptop as the bridge.
Here’s a breakdown of the system:
new xmpp message from me -> xmpp puppet account -> python script/bot -> applescript -> imessage
new xmpp message to me -> imessage -> applescript -> bash script -> xmpp puppet account -> my xmpp account
Sending messages
From an xmpp client, such as Pidgin on linux, I send a message to the account that corresponds to the person I want to message in iMessage. When I message that account, the message is received by a running python script running on my Mac. Currently each individual has their own script running - in the future I’ll condense this into one. This script parses the XML message, grabs just the body (the actual message) and discards the rest (the metadata). It then runs an applescript script, which in turn sends a message to the contact in iMessage.
Some weird stuff
- Messages on my Mac will only trigger the applescript if the app is not the frontmost app. This is weird! I think the idea is that if the app is up front, Messages assumes the user is at the computer and ready to receive messages, and probably doesn’t need any sort of special notification (which is perhaps the intention of the “run script when a message is received”). Until I realized this quirk, the troubleshooting was maddening; sometimes the script was triggered and sometimes it wasn’t - it was due to the front or back status of Messages, but it wasn’t obvious at all.
- Sometimes, but seemingly not everytime , if the script called by iMessages on the receipt of a new message fails, iMessage will reset the script setting. If the script fails, it will be removed, and I have to add it again. This was very frustrating until I realized what was happening!
- This doesn’t seem to work with regular SMS messages. It seems that either iMessage in general or my iMessage in particular doesn’t do SMS.
Receiving messages
When somebody messages me on iMessage, in addition to arriving at my phone, the message is also delivered to the Messages app running on my Mac. Upon receipt, Measages runs an applescript. This applescript grabs the phone number of the contact who messaged me, along with their message, and passes it to a bash script. This bash script parses these items, and through a simple “if…else if…else” decides where to send the message.
If the phone number matches one in the bash script, it delivers it to the appropriate XMPP account on my XMPP server. The bash script messages me by using curl to make a web request to a webhook on my Prosody server (provided by the mod_post_msg plugin).
For instance, if Jane (+16162345678) messages me, the bash script will send a message to me as the account jane@chat.chrisbeckstrom.com. From my perspective inside XMPP, it appears as the user jane@chat.chrisbeckstrom.com has messaged me- just like any other XMPP account. If the phone number does not match one I have in the “if…else if…” list, it sends a message from my general bot (Imabot) to let me know. The message appears to me as something like “Hey, you have a new message from +16169876543: [their message goes here]”. This way, I receive all messages in XMPP, even if I haven’t created a puppet account for the iMessage contact that messaged me.
Future improvements
There is so much to improve! There are a lot of hard-coded details, mostly because I don’t really understand how Python deals with variables (the whole %s thing is confusing to me). I could probably also remove a script or two by using applescript instead of bash to make the web request to my Prosody server. Another improvement would be condensing the multiple python bots into one script.