Copy 2FA code from received iMessage/Text on MacOS

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… :wink:

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.

1 Like

I moved my reply to a new post at Delete political messages from messages app using sql queries

Hi. Can ASObj-C help read NSConcreteAttributedString (e.g. AXAttributedDescription value of a UI element) as String?

No chance. See here:

I’m on macOS Tahoe, and retrieving notification static texts works fine via AppleScript. Let’s take another case then: the small battery widget. Do you think we could retrieve AXAttributedDescription values here with ASObj-C?

I don’t have this widget. As far as I know, Macs only display the battery levels of connected devices, such as AirPods, etc., but not those of the Watch or iPhone. I’ve only seen this feature on iPhones and iPads.

I wouldn’t pursue the UI scripting approach. It’s vulnerable, unstable and changes with every update.

Here is a script that using shell script: to read the battery level from Bluetooth devices.

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

set xmlData to do shell script "system_profiler SPBluetoothDataType -xml"
set theData to ((current application's NSString's stringWithString:xmlData)'s dataUsingEncoding:(current application's NSUTF8StringEncoding))
set infoDict to (current application's NSPropertyListSerialization's propertyListWithData:theData options:0 format:(missing value) |error|:(missing value))

set BatteryLevels to current application's NSMutableDictionary's dictionary()
geBatteryLevels((infoDict's firstObject()'s valueForKey:"_items")'s firstObject()'s valueForKey:"device_connected", true)
geBatteryLevels((infoDict's firstObject()'s valueForKey:"_items")'s firstObject()'s valueForKey:"device_not_connected", false)

return BatteryLevels as record
--> {|airpods pro von dirk|:{BatteryPercentLeft:"95 %", BatteryLevelCase:"84 %", BatteryPercentRight:"95 %", isConnected:*false*}}

on geBatteryLevels(devlist, isConnected)
	if devlist ≠ missing value then
		repeat with j from 0 to (count of (devlist as list)) - 1
			set devName to ((devlist's objectAtIndex:j)'s allKeys()'s objectAtIndex:0)
			set devDic to ((devlist's objectAtIndex:j)'s valueForKey:devName)
			set BatteryLevel to (devDic's valueForKey:"device_batteryLevel")
			set BatteryPercent to (devDic's valueForKey:"device_batteryPercent")
			set BatteryLevelCase to (devDic's valueForKey:"device_batteryLevelCase")
			set BatteryPercentLeft to (devDic's valueForKey:"device_batteryLevelLeft")
			set BatteryPercentRight to (devDic's valueForKey:"device_batteryLevelRight")
			
			set batteryDict to current application's NSMutableDictionary's dictionary()
			
			if BatteryLevel is not missing value then
				(batteryDict's setObject:BatteryLevel forKey:"BatteryLevel")
			end if
			if BatteryPercent is not missing value then
				(batteryDict's setObject:BatteryPercent forKey:"BatteryPercent")
			end if
			if BatteryLevelCase is not missing value then
				(batteryDict's setObject:BatteryLevelCase forKey:"BatteryLevelCase")
			end if
			if BatteryPercentLeft is not missing value then
				(batteryDict's setObject:BatteryPercentLeft forKey:"BatteryPercentLeft")
			end if
			if BatteryPercentRight is not missing value then
				(batteryDict's setObject:BatteryPercentRight forKey:"BatteryPercentRight")
			end if
			
			if (count of batteryDict's allKeys()) > 0 then
				(batteryDict's setObject:isConnected forKey:"IsConnected")
				my (BatteryLevels's setObject:batteryDict forKey:devName)
			end if
		end repeat
	end if
end geBatteryLevels

Only these are displayed in the widget (for me):
image

Thanks. I used a screenshot of the iOS version. Anyway, I only used the small battery widget as an example of SwiftUI elements. I wondered if we could retrieve descriptions from them using AppleScript-ObjC.