It works for me on macOS 14.2.1.
You can check the following:
- The script must be run while the notification is displayed
- Since it’s UI scripting, whatever’s running the script must have Accessibility access granted through System Settings
It works for me on macOS 14.2.1.
You can check the following:
For reasons that are unclear to me, I cannot get the script to work.
Notifications show temporarily on my Mac, but close as soon as I attempt to run the script in Script Debugger.
Script Debugger in System Settings, has accessibility to control both the computer and the full disk.
What else can I change or what script do I need to write to allow Applescript the ability to capture notifications, such as six digit codes sent by web sources?
You can add the following 2 lines to the start of the main() handler definition for easy debugging:
display notification "Testing OTP code with 123456."
delay 1
You can also try toggling the System Settings > Privacy & Security > Accessibilty > Script Debugger permission, and then re-launching Script Debugger in case it’s a permissions bug.
The script doesn’t do anything to close the notification. The OTP should be in your clipboard after the script is run.
Have you previously done anything that affects how long notifications are displayed by the system? You can check the following key (by default, this won’t exist).
defaults read com.apple.notificationcenterui bannerTime
Do you have updated version for 14.4?
When I try your script I get : error “The variable none is not defined.” number -2753 from “none”
I did set the accessibility and I am running while notification is present.
Thanks in advance!
macOS 14.4 hasn’t been released yet. The script is working normally on 14.3.1.
It’s possible there’s some weird kind of terminology clash (because of 14.4, or because of a scripting addition you have installed) with the following line:
set previous_character_is_digit to none
You could try changing this to:
set previous_character_is_digit to missing value
This is exactly why we built flowtext.io.
We got super frustrated with only being able to use iMessage + Safari for this on MacOS so this will automatically copy 2fa codes from iMessage to your clipboard for use in any browser/place you see fit!
Disclosure: It’s $5 for a lifetime license and I’m one of the developers.
Looks like Sequoia has some changes which prevent the AppleScript from working:
error "System Events got an error: Can’t get scroll area 1 of group 1 of window \"Notification Center\" of application process \"NotificationCenter\". Invalid index." number -1719 from scroll area 1 of group 1 of window "Notification Center" of application process "NotificationCenter"
I’ve been trying a few things but I’m just completely guessing as to what I need to change. I have Script Editor set to allowed for controlling my computer through accessibility settings which is what I understand is needed to allow it to work. I also tried enabling Screen and System Audio Recording permissions for Script Editor but that didn’t change anything.
All of my machines are still on Sonoma, so unfortunately I can’t test it at present. GUI scripting frequently breaks with major macOS updates. The only way to fix this is to dig through the view hierarchy of the notification using Script Debugger (or “caveman” style in Script Editor) or Accessibility Inspector.
I’ll be updating to Sequoia around version 15.2. If no one has posted the updated code for Sonoma by the end of the year, please let me know.
I’ve updated to macOS 15.1 Sequoia, and unfortunately it looks like the script is unfixable.
It doesn’t look like the contents of the notifcations are exposed through the Accessibility API anymore. Some of the UI elements are not created until you hover over the notification with the mouse, but even then I can’t find the strings anywhere in the hierachy.
Very disappointing news. I relied on parsing notifications in several scripts. Apple giveth and Apple taketh away.
I’ve been fixing my Notification Center automation as well, and it seems as if the hierarchy of notification windows (buttons) has gotten one deeper. This path works for me:
scroll area 1 of group 1 of group 1 of window "Notification Center"
.
However I no longer seem to be able to access the text contents of the notifications.
These two examples work:
-- Clicks the first notification or group in Sequoia
tell application "System Events" to tell application process "NotificationCenter"
-- clicks the first notification
click button 1 of scroll area 1 of group 1 of group 1 of window "Notification Center"
end tell
-- Clears the top notification in Sequoia
tell application "System Events"
tell process "NotificationCenter"
set SA to scroll area 1 of group 1 of group 1 of window "Notification Center"
if (count (actions of button 1 of SA whose name starts with "Name:Clear All")) is 1 then
-- expand group
click button 1 of SA
delay 0.5
-- close top notification in group
perform first action of (actions of button 3 of SA whose name starts with "Name:Close")
else if description of UI element 1 of SA is "heading" then
-- close top notification in group
perform first action of (actions of button 3 of SA whose name starts with "Name:Close")
else
-- close single top notification
perform first action of (actions of button 1 of SA whose name starts with "Name:Close")
end if
end tell
end tell
But trying to read the description doesn’t work:
-- Can't get any description attribute info in Sequoia
tell application "System Events" to tell application process "NotificationCenter"
-- grab the description of the notification
set desc to attribute "AXAttributedDescription" of button 1 of scroll area 1 of group 1 of group 1 of window "Notification Center"
log (class of desc as text)
-- Both attempts to access the value of the AXAttributedDescription attribute fail
-- with the error: "System Events got an error: AppleEvent handler failed"
log (get value of desc)
log (get value of desc as text)
end tell
Any suggestions would be much appreciated.
(Fully featured notification center control scripts can be found on Github: scripting/applescript at main · seren/scripting · GitHub)
No chance. Apple uses here (possibly deliberately) an NSConcreteAttributedString (private subclass of the NSAttributedString class). There is no equivalent for this in AppleScript. With ASObj-C and the PFAssistive Framework you can at least get to the text, but with comma separation, which can lead to problems if the message itself contains one.
See my Post in the Script Debugger Forum:
Parsing Notifications in macOS Sequoia
Maybe you can do something with it.
Note: You will need the Script Debugger to run the script bundle, as the Script Editor does not support third-party frameworks. A compiled applet runs as normal.
We now have a partial solution that requires an external compiled binary (still under development, but with the basics working) or using ASObjC with the PFAssistive Framework.
Further developments & updates on parsing notifications in macOS 15 Sequoia will be posted on this Late Night Software forum post.
With regards to the original problem, which I appreciate is from a year ago but perhaps still actively being tackled, I would be tempted to retrieve the latest message receivesd within the Messages.app by executing an SQL query on the chat.db
file, e.g.
sqlite3 ~/Library/Messages/chat.db '
SELECT HEX(attributedBody)
FROM message
WHERE is_from_me = 0
ORDER BY date DESC
LIMIT 1'
which returns a string of hexbytes that represent serialised data, which wraps an NSAttributedString
object with an NSString
containing the plain text body of the message.
Nice idea!
It took me a while to get to the text, but it seems to work:
use AppleScript version "2.4"
use framework "Foundation"
use scripting additions
property NSData : a reference to current application's NSData
property NSArray : a reference to current application's NSArray
property NSString : a reference to current application's NSString
property NSUnarchiver : a reference to current application's NSUnarchiver
set hexString to do shell script "sqlite3 ~/Library/Messages/chat.db 'SELECT HEX(attributedBody) FROM message WHERE is_from_me = 0 ORDER BY date DESC LIMIT 1'"
set theData to (NSArray's arrayWithObject:(run script "«data rdat" & hexString & "»"))'s firstObject()'s |data|()
set {theMessage, theError} to (NSUnarchiver's alloc's initForReadingWithData:theData)'s decodeTopLevelObjectAndReturnError:(reference)
if theError is missing value then
return theMessage's |string| as text
end if
--> This is a test message.
CJKm, Dirk, Hirvi74, tree_frog and others’ development of sqlite3 data aquisition from Messages’ chat database is wonderful!
A clumsier method, that I have employed using UI scripting in Messages, delimited text from the first chat in the Messages app. It might be an alternative if the more direct data aquisition method fails.
tell application "System Events"
# Get text of last convesation of chat 1 in Message's UI Window
set ConversationDescription to description of group 1 of ¬
UI element 1 of group 1 of group 1 of group 1 of group 2 ¬
of group 1 of group 1 of group 1 of group 2 of group 1 ¬
of group 1 of group 1 of group 1 of group 1 of group 1 ¬
of group 1 of ¬
window 1 of application process "Messages"
end tell
# set text item delimiters list to text that delimits the authentication or verification code. In this case, text item delimiters were set to a list {" authentication code is ", ".,"}, which were the text item delimiters that preceeded and followed the code.
set text item delimiters to {" authentication code is ", ".,"}
# You might need to change your specific text items to fit the text items that delimit your authentication or verification code.
set AutheticationVerificationCode to text item 2 of ConversationDescription
The text item delimiter portion in the enclosed UI AppleScript, perhaps, may be applied to the sqlite3 data aquisition script.
This AppleScript extracts the code from the last incoming message and copies it to the clipboard. The code must be 4-6 characters long and the text (before the code) must contain Code
, code
, Pin
or pin
and immediately before the code there must be a colon or the word is
or ist
or lautet
followed by a space.
Examples:
Your Apple Account code is: 561583. Do not share it with anyone.
PayPal: Your security-code is: 370626. Do not give this code to anyone.
Amazon: Your code is 458319, do not share it. You did not request it? Decline here: …
Authorize the purchase of 0.0 EUR with your card ***5588. Enter the following code followed by your 3D-Secure code: 7654
Your pin is 458319.
PayPal: Ihr Sicherheitscode lautet: 686747. Geben Sie diesen Code nicht weiter.
Telekom Login: Ihr angeforderter Bestätigungscode lautet 534703. Bitte geben Sie diesen in das Eingabefeld ein. Ihre Telekom
use framework "Foundation"
use scripting additions
property NSData : a reference to current application's NSData
property NSArray : a reference to current application's NSArray
property NSString : a reference to current application's NSString
property NSUnarchiver : a reference to current application's NSUnarchiver
property NSRegularExpression : a reference to current application's NSRegularExpression
set hexString to do shell script "sqlite3 ~/Library/Messages/chat.db 'SELECT HEX(attributedBody) FROM message WHERE is_from_me = 0 ORDER BY date DESC LIMIT 1'"
set theData to (NSArray's arrayWithObject:(run script "«data rdat" & hexString & "»"))'s firstObject()'s |data|()
set {theMessage, theError} to (NSUnarchiver's alloc's initForReadingWithData:theData)'s decodeTopLevelObjectAndReturnError:(reference)
if theError is missing value then
set theString to theMessage's |string|
set thePattern to "(?:(?:Code|code|Pin|pin).*(?:\\:|is|ist|lautet)\\s)(\\d{4,6})"
set theRegEx to NSRegularExpression's regularExpressionWithPattern:thePattern options:0 |error|:(missing value)
set regExResults to theRegEx's matchesInString:theString options:0 range:{location:0, |length|:theString's |length|()}
if regExResults's |count|() > 0 then
set aMatch to regExResults's objectAtIndex:0
if aMatch's numberOfRanges as integer > 1 then
set theRange to (aMatch's rangeAtIndex:1) -- Group 1
if theRange's |length| > 0 then
set theResult to (theString's substringWithRange:theRange) as string
set the clipboard to {text:(theResult as string), Unicode text:theResult}
display notification "Code: " & theResult & " copied." with title "Verification Code"
return
end if
end if
end if
end if
display notification "No code found." with title "Verification Code"
Note: The regex pattern could probably be more optimized, but it works. It’s not necessarily my thing…
Dirk, I enjoyed viewing your Regex addition to match a number pattern in the message text. As verification codes are six digits, would it be more succinct to set a regex pattern for six digits?
set thePattern to “\d{6}”
with the resulting block of code as
if theError is missing value then
set theString to theMessage's |string|
set thePattern to "\\d{6}"
set theRegEx to NSRegularExpression's regularExpressionWithPattern:thePattern options:0 |error|:(missing value)
set regExResults to theRegEx's matchesInString:theString options:0 range:{location:0, |length|:theString's |length|()}
end if
Dirk, I also like your approach to detecting matched ranges and copying them as text to the clipboard. I however was unable to run your script without substituting 0 for 1 in both
aMatch’s numberOfRanges as integer > 1
aMatch’s rangeAtIndex:1
With the resulting Applescript lines of:
if aMatch's numberOfRanges as integer > 0 then
set theRange to (aMatch's rangeAtIndex:0) -- Group 1
My script already recognizes codes with a 4-6 digit number: (\\d{4,6})
My script first checks whether the text contains Code
, Pin
etc. and whether there is a colon etc. in front of the code. The result is then output in a group (in this case Group 1). It must therefore be 1
.
If you change the pattern to a simple match, you must of course use 0
. In this case, however, no check is performed and RegEx simply searches for a 6-digit number. In my opinion, it would be better to adjust the above-mentioned condition in the pattern accordingly if necessary.
You can check the pattern:
(?:(?:Code|code|Pin|pin).*(?:\:|is|ist|lautet)\s)(\d{4,6})
here: https://regex101.com
Dirk, I now understand your method, using your regex pattern, which was to capture various but identifiable texts preceding the 4-6 digit code, and also the 4-6 digit code itself.