You can’t cross the street these days without seeing a new chatbot in development in yet another place
where it’s entirely uneeded. The whole thing reminded me of a simpler time, when something that went where
it wasn’t supposed to, you took a picture of it and captioned “if i fits, i sits”. Meme homebrewed and relevant.
Anyway! How cool would it be if instead of using ChatGPT to write your history papers in the tone of historical figures,
you leveraged it for a far more selfish use case. A personal assistant. I think it’s only a matter of time until
(or maybe it already exists) where your messaging platform drafts your whole response and all you have to do is consent + send = consend it.
Well since apple seems to be sitting on their heels, I took matters into my own hands.
Turns out that if you use MacOS and sync your messages with iCloud, all your messages live in an SQLite folder in ~/Library/Messages/chat.db… unencrypted…
So my plan was to simply query the database periodically for new messages, find a new one, seek the oracle (aka gpt-3) for wisdom, and then send that.
It turns out… it’s entirely that easy. I will still walk you through the project but if you don’t want to read anymore, check out the github here.
So the first step is understanding the schema of chat.db so it can be queried. I played around with DB Browser for SQLite and came up with the following query that I was happy with:
Which works awesome. Except if you run it on your own machine you’ll notice two things… first the text field is often null, second the results are missing group messages. I only noticed the groupchat one because I have a motivation message automation I send my friends every morning at 6am and noticed it was missing.
While scanning the messages table I found that the missing texts are located in the attributedBody field as a binary blob embedded in what looks like swift code, I’ve underlined one example in red. Also the groupchat I thought was missing is underlined in green.
Groupchats were missing simply because handle.id is null for groupchat. By searching the whole database for groupchat name that I know I have I found that the name is stored in the chat table and the problem was solved in SQL with the addition of LEFT JOIN chat ON message.cache_roomnames=chat.chat_identifier.
I unfortunately don’t grok SQL enough to even begin to know if I could extract the text from the attributedBody field so it is time to introduce python. I found imessage_reader as a great starting point - I was originally going to write my own little library but I have been trying to read more code and contribute to open source more so off the shelf the solution it is. Looking at the query inside imessage_reader, in the fetch_data.py file:
looks like we want to update the _read_database function. Just as a test, I ran the library as is and counted how many text fields had a null value, and off the 90k record I had, 10k were null. Wow.
By carefully examining the output from the attributedBody field I was able to see that they all followed a general pattern of:
streamtypedè@NSMutableAttributedStringNSAttributedStringNSObject NSMutableStringNSString+-THE TEXT MESSAGEiI-NSDictionaryi__kIMMessagePartAttributeNameNSNumberNSValue*
with some small inconsistencies. Most notably, I noticed that the byte(s) preceeding the text were often the length of the text, so I am assuming that this is a binary dump of the swift object that holds the text message. By sheer amount of time examining the ouput I noticed that if the bytes began with '\x81' then the length of the text was stored as two bytes in little endianness and otherwise the length was stored as a single byte. So after stripping some of the leading junk I came up with this new function:
The parsing worked on all but 190 text messages, which on a query of 90k, I was okay with. I put the code in a pull request, and the author said “good addition” when they accepted and merged it. :satisfaction.gif:
Finally to makeimessage_reader usuable I added some functionality to parameterize the query, currently the library returns a list of lists of every row in chat.db, I absolutely don’t want to load 90k text records every query and then sort them in python. I think I’ll just let SQL do that. My plan is to check for new text messages every minute so I added a get_messages_between_dates function.
I am curious how more experienced developers would go about augmenting the SQL query when encountering a codebase like this. Doing string concatenation with the SQL_CMD variable doesn’t sit entirely right but I am planning on eventually making a pull request with these additions and didn’t want to stray too far from the authors codebase.
We’re finally at a place where we can programmatically check for new text messages. With the additions to imessage_reader we can psuedo code how the script is going to work
Filling in the blanks and adding a config file with our api keys, a gpt prompt and some other settings gets us to:
Oh - the reason I get to glaze over how to programmatically send an iMessage is because this repo’s solution works beautifully.
This is great. Right now anybody in the groupchat can summon gpt by adding !bot to their text. This use case is just like the discord chatbots I’ve seen. Everyone in my friend group had fun asking gpt questions at my API key’s expense.
However, my original goal was to have GPT as my secretary. The way envision that is - when I go to work I set my phone on Do Not Disturb, I’d like my program to read my focus state and then let people reaching out know that I am currently unavailable. I also wanted to make the responses fun so I’ve included a the EmojiPasta package to make the responses ✨pretty✨. Turns out that your do not disturb state is stored in a json file in ~/Library/DoNotDisturb/DB/Assertsions.json so it can be grabbed with the following code: