Search code examples
macosapplescriptimessage

Reading iMessages with AppleScript


I'm trying to write a script that will go through all the chats in the Messages app - my goal is to find all messages I haven't replied to and send me an reminder.

But I'm stuck at square one - I can see how many message I have:

tell application "Messages" to log (count of chats)

But I can't get even simple properties out of a chat; for example:

tell application "Messages" to set x to started of first chat

gives the error: Can’t get started of chat 1." number -1728 from started of chat 1

Any ideas where I'm going wrong...?


Solution

  • I used your sqlite3 code with a few minor tweaks, then created this AppleScript, which generates a list of reminders (in the Reminders app). The only downside is that the senders appear as phone numbers or iMessage addresses, and don't get "converted" to names using your address book. This might be possible to achieve through some tell commands to Contacts and doing a search against phone numbers, but that's beyond the scope of this particular endeavour for now.

        -- Run an SQL query on the messages database via shell script
        do shell script (["sqlite3 -line ~/Library/Messages/chat.db ", ¬
            "'SELECT MAX(date) lastMessageDate, h.id, text ", ¬
            "FROM message m INNER JOIN handle h ON h.ROWID=m.handle_id ", ¬
            "WHERE is_from_me = 0 GROUP BY h.ROWID' | egrep -io -e '\\w+ = .+'"] as text)
        
        set query_response to result
            --> lastMessageDate = %timestamp%
            --> id = %phone number or iMessage address%
            --> text = %message content%
            --> ...
        
        -- Groups all messages into a single item of an array
        -- Each occurence of  "lastMessageDate = " indicates 
        -- a new message and, hence, a new array item.
        set AppleScript's text item delimiters to "lastMessageDate = "
        set query_response to rest of text items of query_response
        
        -- Loop through the array organise each message
        repeat with message in query_response
            
            -- Split the array item up into its components
            set [receivedOn, sentFrom, textContent] ¬
                to [paragraph 1, ¬
                text 6 thru -1 of paragraph 2, ¬
                text 8 thru -1 of paragraph 3] of message
            
            -- Convert ReceivedOn to a value representing
            -- the number of days since 01/01/2001
            set receivedOn to receivedOn / (1.0E+9 * 86400)
            
            -- Convert ReceivedOn again to the date
            -- corresponding to the number of days since
            -- 01/01/2001, i.e. the date and time the message
            -- was sent
            set receivedOn to (date "Monday, 1 January 2001 at 00:00:00") + receivedOn * days
            
            -- Make a reminder
            tell application "Reminders"
                if not (exists (list named "Awaiting My Reply")) then ¬
                    make new list with properties {name:"Awaiting My Reply"}
                
                tell list named "Awaiting My Reply"
                    -- This creates a reminder with a past due date
                    -- meaning you can sort the reminder list by due
                    -- date to see who has been waiting longest
                    -- for you to reply
                    make new reminder with properties ¬
                        {name:"Reply To " & sentFrom, body:¬
                            textContent, due date:¬
                            receivedOn} ¬
                            
                end tell
            end tell
        end repeat
    

    Of course, if any of your text messages happen to contain the phrase "lastMessagedate = ", then that will not work well with the script above. However, there are workarounds for this, and I experimented with various formats returned by sqlite3 including list, column, and csv, all of which have their advantages and disadvantages.

    ADDENDUM: Retrieving contact details from a matching phone number

    After much experimentation, have managed to integrate the above script with an address book search to replace phone numbers with contacts' names:

        -- Replace dbID with your address book's serial number
        -- (look in ~/Library/Application Support/AddressBook/Sources)
        property dbID : "377FDA5C-013A-430A-A964-9943C0B40137"
        property db : {Messages:"/Users/CK/Library/Messages/chat.db", Addresses:"/Users/CK/Library/Application Support/AddressBook/Sources/" & dbID & "/AddressBook-v22.abcddb"}
    
        -- Run an SQL query on the messages database via shell script
        set Messages to do shell script (["sqlite3 -line ", ¬
            quoted form of (Messages in db) as text, ¬
            " 'SELECT MAX(date) lastMessageDate, h.id, text", ¬
            " FROM message m INNER JOIN handle h ON h.ROWID=m.handle_id", ¬
            " WHERE is_from_me = 0 GROUP BY h.ROWID'", ¬
            " | egrep -io -e '\\w+ = .+'"] as text)
        --> lastMessageDate = %timestamp%
        --> id = %phone number or iMessage address%
        --> text = %message content%
        --> ...
        
        set PhoneNumbers to do shell script (["sqlite3 -list -separator : ", ¬
            quoted form of (Addresses in db) as text, ¬
            " 'select r.ZFIRSTNAME, r.ZLASTNAME, p.ZFULLNUMBER", ¬
            " from ZABCDRECORD as r, ZABCDPHONENUMBER as p", ¬
            " WHERE p.ZOWNER=r.Z_PK'", ¬
            " | tr -c -d '[:alnum:]:[:cntrl:]:[:space:]:+#*'"] as text)
        --> %first name%:%last name:%phone number%
        --> ...
        
        set NamesAndNumbers to paragraphs of PhoneNumbers
        set AppleScript's text item delimiters to ":"
        tell NamesAndNumbers to ¬
            repeat with n from 1 to count it
                set its item n to ¬
                    {firstname:text item 1, lastname:text item 2, phone:text item 3} ¬
                        of its item n
            end repeat
        
        -- Groups all messages into a single item of an array
        -- Each occurence of  "lastMessageDate = " indicates 
        -- a new message and, hence, a new array item.
        set AppleScript's text item delimiters to "lastMessageDate = "
        set Messages to rest of text items of Messages
        
        -- Loop through the array organise each message
        repeat with message in Messages
            
            -- Split the array item up into its components
            set [receivedOn, sentFrom, textContent] ¬
                to [paragraph 1, ¬
                text 6 thru -1 of paragraph 2, ¬
                text 8 thru -1 of paragraph 3] of message
            
            -- Convert ReceivedOn to a value representing
            -- the number of days since 01/01/2001
            set receivedOn to receivedOn / (1.0E+9 * 86400)
            
            -- Convert ReceivedOn again to the date
            -- corresponding to the number of days since
            -- 01/01/2001, i.e. the date and time the message
            -- was sent
            set receivedOn to (date "Monday, 1 January 2001 at 00:00:00") + receivedOn * days
            
            -- Keep the "+" delimiter but replace the "+44" with
            -- your own country's intl dialling code, e.g. {"+1", "+"}
            -- as this may or may not prefix a phone number
            set AppleScript's text item delimiters to {"+44", "+"}
            set sentFrom to last text item of sentFrom
            
            -- Find the name to whom the phone number belongs
            ignoring white space
                set match to ""
                repeat with person in NamesAndNumbers
                    if the person's phone contains sentFrom then
                        set match to contents of the person
                        exit repeat
                    end if
                end repeat
            end ignoring
            
            set AppleScript's text item delimiters to space
            if match is not "" then set sentFrom to the contents of ¬
                {firstname, lastname} of match as text
            
            -- Make a reminder
            tell application "Reminders"
                if not (exists (list named "Awaiting My Reply")) then ¬
                    make new list with properties {name:"Awaiting My Reply"}
                
                tell list named "Awaiting My Reply"
                    -- This creates a reminder with a past due date
                    -- meaning you can sort the reminder list by due
                    -- date to see who has been waiting longest
                    -- for you to reply
                    make new reminder with properties ¬
                        {name:"Reply To " & sentFrom, body:¬
                            textContent, due date:¬
                            receivedOn} ¬
                            
                end tell
            end tell
        end repeat