Is it possible to react to the receipt of an apple message?

TL;DR : within BTT, is it possible to react when an Apple Message is received? If so, how?

Hi folks,

First time poster so please be gentle if you can. This said, I’m an old coder so I’m familiar with this sort of tomfoolery and the terminology etc.

A bit of context: my family uses iPhones but I use Android[+iPad+MBP] and so messages get missed, confusion reigns and so on. I want to build a rudimentary bridge if you will between Android and Apple Messages.

I have the send-apple-message-from-android Use Case working via BTT’s web server & invoking named triggers from MacroDroid on the Android phone - but I can’t see how I can react-to / handle an Apple Message being received.

Any pointers, ideally using BTT, but via any means if necessary - would be greatly, greatly appreciated.

Cheers,
Pete

Unfortunately not really, there is no way to listen to notifications for such messages and they are guarded from external access ;-(

1 Like

Andreas! No less than the main man himself! - thank you so much for giving us all such a great MacOS penknife, truly. I love eliminating toil from my life and BTT's a champ at that, so :pray:t2:

To business: that's what I feared. Drat. I will start looking into those "automated" Shortcuts in the iOS Shortcuts.app but I'm not holding my breath.

If I were a cynical man (which I happen to be lol) I'd wonder if Apple provided no means to do as I require in order to safeguard against 3rd party Apple Messages apps and the like...

Cheers, Pete

Hi @sm1ng,

If you’re still exploring solutions for triggering actions when an Apple Message (iMessage or SMS) is received, I wanted to share a sample script that might help. This script monitors your Messages database for new incoming messages by comparing the current count of messages with a stored count from the previous run. When it detects a new message, it displays a desktop notification. You can easily customize the notification or add your own custom logic.

Below is the JavaScript code that you can run with BetterTouchTool’s Run Real JavaScript action:

(async () => {
	// Retrieve the stored count of received messages from BetterTouchTool's persistent storage.
	// This value keeps track of how many messages (that are not sent by me) have been previously received.
	let previousNumberOfMessages = await get_number_variable({
		variable_name: "number_of_messages",
	});

	// If the persisted message count is not defined (e.g., on the first run),
	// initialize it to 0 to establish a starting point.
	if (previousNumberOfMessages === undefined) {
		previousNumberOfMessages = 0;
		await set_persistent_number_variable({
			variable_name: "number_of_messages",
			to: previousNumberOfMessages,
		});
	}

	// Define a shell command to count the number of received messages (messages where is_from_me equals 0)
	// from the chat database. This command uses sqlite3 to execute the query on the Messages database.
	let countRowsShellScript =
		'sqlite3 "$HOME/Library/Messages/chat.db" "SELECT COUNT(*) FROM message WHERE is_from_me = 0;"';

	// Execute the shell script and capture the current total number of incoming messages.
	let numberOfMessages = await runShellScript({
		script: countRowsShellScript,
	});

	// Check if a new message has been received by comparing the current count with the stored count.
	if (numberOfMessages > previousNumberOfMessages) {
		// Update the persistent storage with the new message count.
		await set_persistent_number_variable({
			variable_name: "number_of_messages",
			to: numberOfMessages,
		});

		// Construct a shell command to fetch the most recent incoming message.
		// This command selects the latest message where is_from_me is 0, outputs it as JSON,
		// and uses jq to neatly extract the first (and only) object from the returned JSON array.
		let getLastMessageShellScript = `sqlite3 -json $HOME/Library/Messages/chat.db "SELECT * FROM message WHERE is_from_me = 0 ORDER BY date DESC LIMIT 1;" | /opt/homebrew/bin/jq '.[0]'`;

		// Run the shell command to retrieve the last received message.
		let lastMessage = await runShellScript({
			script: getLastMessageShellScript,
		});

		// Convert the JSON string received from the shell command into a JavaScript object.
		let lastMessageObject = JSON.parse(lastMessage);

		// Extract the text from the latest message.
		// Note: Some messages (like audio messages or reactions) may not contain textual content.
		let text = lastMessageObject.text || "No text";

		// Display a desktop notification to inform the user about the new incoming message.
		// If the message has no text, a default fallback ("No text") is shown.
		await display_notification({
			title: "New message!",
			subTitle: `${text}`,
		});
	}

	// Return the current message count back to BetterTouchTool,
	// useful for debugging and testing.
	returnToBTT(numberOfMessages);
})();

And here’s the corresponding BetterTouchTool configuration JSON. This setup uses a Repeating or Time Based Trigger (set to every 5 seconds) to run the above JavaScript code:

[
  {
    "BTTActionCategory" : 0,
    "BTTLastUpdatedAt" : 1740813597.1263862,
    "BTTTriggerType" : 678,
    "BTTTriggerTypeDescriptionReadOnly" : "Repeating or Time Based Trigger",
    "BTTTriggerClass" : "BTTTriggerTypeOtherTriggers",
    "BTTUUID" : "B395EECB-0330-4BC8-8E35-2EC839B3BCEB",
    "BTTPredefinedActionType" : 366,
    "BTTPredefinedActionName" : "Empty Placeholder",
    "BTTAdditionalConfiguration" : "{\"BTTTimedRepeatEveryXSeconds\":\"5\",\"BTTTimedWhenToTrigger\":0}",
    "BTTEnabled" : 1,
    "BTTEnabled2" : 1,
    "BTTOrder" : 11,
    "BTTAdditionalActions" : [
      {
        "BTTActionCategory" : 0,
        "BTTLastUpdatedAt" : 1740813588.787029,
        "BTTTriggerParentUUID" : "B395EECB-0330-4BC8-8E35-2EC839B3BCEB",
        "BTTIsPureAction" : true,
        "BTTTriggerClass" : "BTTTriggerTypeOtherTriggers",
        "BTTUUID" : "13FEDBDA-6255-4098-B312-BB1BC6186DF1",
        "BTTPredefinedActionType" : 281,
        "BTTPredefinedActionName" : "Run Real JavaScript",
        "BTTAdditionalActionData" : {
          "BTTScriptFunctionToCall" : "someJavaScriptFunction",
          "BTTJavaScriptUseIsolatedContext" : false,
          "BTTAppleScriptRunInBackground" : false,
          "BTTScriptType" : 3,
          "BTTAppleScriptString" : "(async () => {\n\t\/\/ Retrieve the stored count of received messages from BetterTouchTool's persistent storage.\n\t\/\/ This value keeps track of how many messages (that are not sent by me) have been previously received.\n\tlet previousNumberOfMessages = await get_number_variable({\n\t\tvariable_name: \"number_of_messages\",\n\t});\n\n\t\/\/ If the persisted message count is not defined (e.g., on the first run),\n\t\/\/ initialize it to 0 to establish a starting point.\n\tif (previousNumberOfMessages === undefined) {\n\t\tpreviousNumberOfMessages = 0;\n\t\tawait set_persistent_number_variable({\n\t\t\tvariable_name: \"number_of_messages\",\n\t\t\tto: previousNumberOfMessages,\n\t\t});\n\t}\n\n\t\/\/ Define a shell command to count the number of received messages (messages where is_from_me equals 0)\n\t\/\/ from the chat database. This command uses sqlite3 to execute the query on the Messages database.\n\tlet countRowsShellScript =\n\t\t'sqlite3 \"$HOME\/Library\/Messages\/chat.db\" \"SELECT COUNT(*) FROM message WHERE is_from_me = 0;\"';\n\n\t\/\/ Execute the shell script and capture the current total number of incoming messages.\n\tlet numberOfMessages = await runShellScript({\n\t\tscript: countRowsShellScript,\n\t});\n\n\t\/\/ Check if a new message has been received by comparing the current count with the stored count.\n\tif (numberOfMessages > previousNumberOfMessages) {\n\t\t\/\/ Update the persistent storage with the new message count.\n\t\tawait set_persistent_number_variable({\n\t\t\tvariable_name: \"number_of_messages\",\n\t\t\tto: numberOfMessages,\n\t\t});\n\n\t\t\/\/ Construct a shell command to fetch the most recent incoming message.\n\t\t\/\/ This command selects the latest message where is_from_me is 0, outputs it as JSON,\n\t\t\/\/ and uses jq to neatly extract the first (and only) object from the returned JSON array.\n\t\tlet getLastMessageShellScript = `sqlite3 -json $HOME\/Library\/Messages\/chat.db \"SELECT * FROM message WHERE is_from_me = 0 ORDER BY date DESC LIMIT 1;\" | \/opt\/homebrew\/bin\/jq '.[0]'`;\n\n\t\t\/\/ Run the shell command to retrieve the last received message.\n\t\tlet lastMessage = await runShellScript({\n\t\t\tscript: getLastMessageShellScript,\n\t\t});\n\n\t\t\/\/ Convert the JSON string received from the shell command into a JavaScript object.\n\t\tlet lastMessageObject = JSON.parse(lastMessage);\n\n\t\t\/\/ Extract the text from the latest message.\n\t\t\/\/ Note: Some messages (like audio messages or reactions) may not contain textual content.\n\t\tlet text = lastMessageObject.text || \"No text\";\n\n\t\t\/\/ Display a desktop notification to inform the user about the new incoming message.\n\t\t\/\/ If the message has no text, a default fallback (\"No text\") is shown.\n\t\tawait display_notification({\n\t\t\ttitle: \"New message!\",\n\t\t\tsubTitle: `${text}`,\n\t\t});\n\t}\n\n\t\/\/ Return the current message count back to BetterTouchTool,\n\t\/\/ useful for debugging and testing.\n\treturnToBTT(numberOfMessages);\n})();\n",
          "BTTActionJSRunInSeparateContext" : false,
          "BTTAppleScriptUsePath" : false,
          "BTTScriptLocation" : 0
        },
        "BTTRealJavaScriptString" : "(async () => {\n\t\/\/ Retrieve the stored count of received messages from BetterTouchTool's persistent storage.\n\t\/\/ This value keeps track of how many messages (that are not sent by me) have been previously received.\n\tlet previousNumberOfMessages = await get_number_variable({\n\t\tvariable_name: \"number_of_messages\",\n\t});\n\n\t\/\/ If the persisted message count is not defined (e.g., on the first run),\n\t\/\/ initialize it to 0 to establish a starting point.\n\tif (previousNumberOfMessages === undefined) {\n\t\tpreviousNumberOfMessages = 0;\n\t\tawait set_persistent_number_variable({\n\t\t\tvariable_name: \"number_of_messages\",\n\t\t\tto: previousNumberOfMessages,\n\t\t});\n\t}\n\n\t\/\/ Define a shell command to count the number of received messages (messages where is_from_me equals 0)\n\t\/\/ from the chat database. This command uses sqlite3 to execute the query on the Messages database.\n\tlet countRowsShellScript =\n\t\t'sqlite3 \"$HOME\/Library\/Messages\/chat.db\" \"SELECT COUNT(*) FROM message WHERE is_from_me = 0;\"';\n\n\t\/\/ Execute the shell script and capture the current total number of incoming messages.\n\tlet numberOfMessages = await runShellScript({\n\t\tscript: countRowsShellScript,\n\t});\n\n\t\/\/ Check if a new message has been received by comparing the current count with the stored count.\n\tif (numberOfMessages > previousNumberOfMessages) {\n\t\t\/\/ Update the persistent storage with the new message count.\n\t\tawait set_persistent_number_variable({\n\t\t\tvariable_name: \"number_of_messages\",\n\t\t\tto: numberOfMessages,\n\t\t});\n\n\t\t\/\/ Construct a shell command to fetch the most recent incoming message.\n\t\t\/\/ This command selects the latest message where is_from_me is 0, outputs it as JSON,\n\t\t\/\/ and uses jq to neatly extract the first (and only) object from the returned JSON array.\n\t\tlet getLastMessageShellScript = `sqlite3 -json $HOME\/Library\/Messages\/chat.db \"SELECT * FROM message WHERE is_from_me = 0 ORDER BY date DESC LIMIT 1;\" | \/opt\/homebrew\/bin\/jq '.[0]'`;\n\n\t\t\/\/ Run the shell command to retrieve the last received message.\n\t\tlet lastMessage = await runShellScript({\n\t\t\tscript: getLastMessageShellScript,\n\t\t});\n\n\t\t\/\/ Convert the JSON string received from the shell command into a JavaScript object.\n\t\tlet lastMessageObject = JSON.parse(lastMessage);\n\n\t\t\/\/ Extract the text from the latest message.\n\t\t\/\/ Note: Some messages (like audio messages or reactions) may not contain textual content.\n\t\tlet text = lastMessageObject.text || \"No text\";\n\n\t\t\/\/ Display a desktop notification to inform the user about the new incoming message.\n\t\t\/\/ If the message has no text, a default fallback (\"No text\") is shown.\n\t\tawait display_notification({\n\t\t\ttitle: \"New message!\",\n\t\t\tsubTitle: `${text}`,\n\t\t});\n\t}\n\n\t\/\/ Return the current message count back to BetterTouchTool,\n\t\/\/ useful for debugging and testing.\n\treturnToBTT(numberOfMessages);\n})();\n",
        "BTTEnabled" : 1,
        "BTTEnabled2" : 1,
        "BTTOrder" : 726
      }
    ]
  }
]

This configuration continuously checks for new messages every 5 seconds and triggers the JavaScript, making it a flexible starting point for further customization.

Hope this helps!

3 Likes

nice! I‘ll take your logic and make it into a native integrated trigger

I assume this requires Full Disk Access permission, correct?

2 Likes

Yes it does :+1:

I'm also working on improving the script, specifically the sqlite3 commands, to obtain more data about the message, e.g. sender name, sender contact details, etc.

EDIT:
as well as parsing Apple's typedstream format :sweat_smile:
https://chrissardegna.com/blog/reverse-engineering-apples-typedstream-format/

Small improvement to the script. The script now handles all messages since the last check instead of assuming only one message was received since the last check.

(async () => {
	// Retrieve the stored count of received messages from BetterTouchTool's persistent storage.
	// This value keeps track of how many messages (that are not sent by me) have been previously received.
	let previousNumberOfMessages = await get_number_variable({
		variable_name: "number_of_messages",
	});

	// If the persisted message count is not defined (e.g., on the first run),
	// initialize it to 0 to establish a starting point.
	if (previousNumberOfMessages === undefined) {
		previousNumberOfMessages = 0;
		await set_persistent_number_variable({
			variable_name: "number_of_messages",
			to: previousNumberOfMessages,
		});
	}

	// Define a shell command to count the number of received messages (messages where is_from_me equals 0)
	// from the chat database. This command uses sqlite3 to execute the query on the Messages database.
	let countRowsShellScript =
		'sqlite3 "$HOME/Library/Messages/chat.db" "SELECT COUNT(*) FROM message WHERE is_from_me = 0;"';

	// Execute the shell script and capture the current total number of incoming messages.
	let numberOfMessages = await runShellScript({
		script: countRowsShellScript,
	});

	// Check if a new message has been received by comparing the current count with the stored count.
	if (numberOfMessages > previousNumberOfMessages) {
		// Update the persistent storage with the new message count.
		await set_persistent_number_variable({
			variable_name: "number_of_messages",
			to: numberOfMessages,
		});

		// Calculate the difference between the current and previous message counts.
		// This difference represents the number of new messages received since the last check.
		let numberOfMessagesDifference =
			numberOfMessages - previousNumberOfMessages;

		// Construct a shell command to fetch the most recent incoming message.
		// This command selects the latest message where is_from_me is 0, outputs it as JSON,
		// and uses jq to extract all of the messages received since the last check.

		let getLastMessageShellScript = `sqlite3 -json $HOME/Library/Messages/chat.db "SELECT * FROM message WHERE is_from_me = 0 ORDER BY date DESC LIMIT ${numberOfMessagesDifference};" | /opt/homebrew/bin/jq`;

		// Run the shell command to retrieve the last received message.
		let lastMessages = await runShellScript({
			script: getLastMessageShellScript,
		});

		// Convert the JSON string received from the shell command into a JavaScript object.
		let lastMessagesObject = JSON.parse(lastMessages);

		for (let message of lastMessagesObject) {
			// Extract the text from the latest message.
			// Note: Some messages (like audio messages or reactions) may not contain textual content.
			let text = message.text || "No text";

			// Display a desktop notification to inform the user about the new incoming message.
			// If the message has no text, a default fallback ("No text") is shown.
			await display_notification({
				title: "New message!",
				subTitle: `${text}`,
			});
		}
	}

	// Return the current message count back to BetterTouchTool,
	// useful for debugging and testing.
	returnToBTT(numberOfMessages);
})();

1 Like

Sneak peak at the next update... :wink:

Hey @fortred2 , you’ve really gone to town on this one, I love it :smiley: !!!

I don’t see any obvious bugs FWIW (I’m a backend dude with some JS experience is all). One naming thing I might suggest is renaming numberOfMessagesDifference to numberOfNewMessages?

Anyway, pedantry aside, what Use Cases do you have in mind for this functionality? It’s so long ago that I barely remember, but I believe I was simply going to send any Apple Messages to the pushbullet (I LOVE pushbullet BTW, it’s sooo reliable and free) API and thus get a notification on my Android phone.

The bummer in all this is that it’d require I leave my MBP on and awake 24/7 which would suck. Hmm… I do have a Pi 5 tho. But I’m guessing that getting BTT working on the Pi is rather low down on the priority list :smiley:

Thanks for running with this, I’m currently not very well but if I improve, I will take a stab at getting this going for sure. Are you going to put it on GitHub or as a gist or something? It’d be kinda cool to put on your portfolio, right?

Cheers.

@sm1ng I'm glad you appreciate it!

numberOfNewMessages is a good suggestion – shorter and improved semantically.

I've never used Pushbullet but I have used Pushover, they look similar. I haven't used it in a while but I created several really cool projects with it.

My current idea is to create an AI personal messaging assistant that can help me stay on top of messaging. I'm not always the best at responding to people promptly haha. I'm experimenting using https://n8n.io/ (hosted locally) and GitHub - agno-agi/agno: Build Multimodal AI Agents with memory, knowledge and tools. Simple, fast and model-agnostic. that defines an AI agent that has access to tools like iMessage, email, WhatsApp, and Instagram direct messages. My AI personal messaging assistant will be able to be triggered when new messages are received as well as be triggered when, for example, I don't reply to someone after a certain amount of time.

Indeed, this currently requires needing to leave a MBP on and awake 24/7 to work. It would be very cool to have it run on a Pi. The problem is that the Pi needs access to the ~/Library/Messages/chat.db database which is located on your Mac.

I looked into seeing if Messages are accessible on the iCloud web app to see if we could intercept the HTTP request to reverse engineer the API (see GitHub - picklepete/pyicloud: A Python + iCloud wrapper to access iPhone and Calendar data.). Unfortunately Messages are not accessible via the iCloud web app.

My last remaining idea to address the "MBP on and awake 24/7" problem is to see if the ~/Library/Messages/chat.db also exists on iPhones. If so, we could purchase a cheap old iPhone for ~$100 and use a combination of Pushcut Automation Server and pymobiledevice3 to query the database and detect new messages. I have yet to confirm if this is possible yet. If it is, we could leave the old iPhone turned on and running 24/7 like a Pi.

I'll let you know if I put this script up on my Github!

Hope you feel better soon and looking forward to hearing what cool things you make with this!

There's a possible alternative to leaving your Mac on 24/7. MacOS's "Power Nap" + "Wake for network access" features should allow the mac to receieve messages, even while asleep: https://i.imgur.com/V31SLqX.png

From there, if you "Share Resources" on the Mac, it should wake up and respond. So you could do one of the following:

  1. Share the ~/Library/messages folder via SMB or AFP.
  2. Enable "Remote Login" on the Mac to enable ssh access.

Then, have the Raspberry Pi run a script on a cron to rysnc the directory or read it, whatever works for you.

I obviouly haven't tested this and it won't work if the Mac leaves your network, but it's something...

2 Likes

Neat idea. I'll look into this – thanks!

1 Like

Yw! Let me know if it pans out! :crossed_fingers: