fix: reject unsupported custom emoji reactions

This commit is contained in:
Peter Steinberger 2026-05-05 01:22:07 +01:00
parent 715a75fb4e
commit df2d928ff0
No known key found for this signature in database
5 changed files with 39 additions and 50 deletions

View File

@ -21,6 +21,7 @@
- fix: confirm standard tapback reaction selection in Messages automation (#53, thanks @PeterRosdahl)
- fix: gate RPC watch reaction metadata on `include_reactions`, not `attachments` (#82)
- fix: dedupe URL balloon preview duplicates in watch stream without cross-chat/schema regressions (#64, thanks @lesaai)
- fix: reject unsupported custom emoji reaction sends instead of emitting a no-op AppleScript path (#55)
- fix: normalize IMCore typing chat lookup across `iMessage`, `SMS`, and `any` prefixes (#51, #54, #56, #58)
- docs: document macOS 26 advanced IMCore injection limits (#60)
- fix: report macOS 26/Tahoe IMCore typing entitlement failures as advanced-feature setup errors (#60)

View File

@ -91,6 +91,10 @@ imsg launch
imsg typing --to "+14155551212" --duration 5s
```
`imsg react` sends only the standard tapbacks exposed reliably through
Messages.app automation. Custom emoji tapbacks can be read from history/watch
output, but are not sent by the CLI.
## Attachment notes
`--attachments` prints per-attachment lines with name, MIME, missing flag, and resolved path (tilde expanded). By default only metadata is shown; files arent copied.

View File

@ -8,6 +8,7 @@ public enum IMsgError: LocalizedError, Sendable {
case appleScriptFailure(String)
case typingIndicatorFailed(String)
case invalidReaction(String)
case unsupportedReaction(String)
case chatNotFound(chatID: Int64)
public var errorDescription: String? {
@ -45,8 +46,9 @@ public enum IMsgError: LocalizedError, Sendable {
Invalid reaction: \(value)
Valid reactions: love, like, dislike, laugh, emphasis, question
Or use an emoji for custom reactions (e.g., 🎉)
"""
case .unsupportedReaction(let message):
return "Unsupported reaction: \(message)"
case .chatNotFound(let chatID):
return "Chat not found: \(chatID)"
}

View File

@ -16,7 +16,9 @@ enum ReactCommand {
Reaction types:
love (), like (👍), dislike (👎), laugh (😂), emphasis (), question ()
Or any single emoji for custom reactions (iOS 17+ / macOS 14+)
Custom emoji tapbacks can be read from history/watch output, but cannot be
sent reliably through Messages.app AppleScript automation.
""",
signature: CommandSignatures.withRuntimeFlags(
CommandSignature(
@ -24,7 +26,7 @@ enum ReactCommand {
.make(label: "chatID", names: [.long("chat-id")], help: "chat rowid to react in"),
.make(
label: "reaction", names: [.long("reaction"), .short("r")],
help: "reaction type: love, like, dislike, laugh, emphasis, question, or emoji"),
help: "reaction type: love, like, dislike, laugh, emphasis, question"),
],
flags: []
)
@ -32,7 +34,6 @@ enum ReactCommand {
usageExamples: [
"imsg react --chat-id 1 --reaction like",
"imsg react --chat-id 1 -r love",
"imsg react --chat-id 1 -r 🎉",
]
) { values, runtime in
try await run(values: values, runtime: runtime)
@ -58,6 +59,12 @@ enum ReactCommand {
if case .custom(let emoji) = reactionType, !isSingleEmoji(emoji) {
throw IMsgError.invalidReaction(reactionString)
}
if case .custom(let emoji) = reactionType {
throw IMsgError.unsupportedReaction(
"custom emoji tapback '\(emoji)' cannot be sent by Messages.app "
+ "AppleScript automation; use love, like, dislike, laugh, emphasis, or question."
)
}
// Get chat info for the GUID
let dbPath = values.option("db") ?? MessageStore.defaultPath
@ -103,40 +110,11 @@ enum ReactCommand {
case .laugh: keyNumber = 4
case .emphasis: keyNumber = 5
case .question: keyNumber = 6
case .custom:
let script = """
on run argv
set chatGUID to item 1 of argv
set chatLookup to item 2 of argv
set customEmoji to item 3 of argv
tell application "Messages"
activate
set targetChat to chat id chatGUID
end tell
delay 0.3
tell application "System Events"
tell process "Messages"
keystroke "f" using command down
delay 0.15
keystroke "a" using command down
keystroke chatLookup
delay 0.25
key code 36
delay 0.35
keystroke "t" using command down
delay 0.2
keystroke customEmoji
delay 0.1
key code 36
end tell
end tell
end run
"""
try appleScriptRunner(script, [chatGUID, chatLookup, reactionType.emoji])
return
case .custom(let emoji):
throw IMsgError.unsupportedReaction(
"custom emoji tapback '\(emoji)' cannot be sent by Messages.app "
+ "AppleScript automation; use love, like, dislike, laugh, emphasis, or question."
)
}
let script = """

View File

@ -60,7 +60,7 @@ func reactCommandBuildsParameterizedAppleScriptForStandardTapback() async throws
}
@Test
func reactCommandBuildsParameterizedAppleScriptForCustomEmoji() async throws {
func reactCommandRejectsCustomEmojiSend() async throws {
let path = try CommandTestDatabase.makePath()
let values = ParsedValues(
positional: [],
@ -68,21 +68,25 @@ func reactCommandBuildsParameterizedAppleScriptForCustomEmoji() async throws {
flags: []
)
let runtime = RuntimeOptions(parsedValues: values)
var capturedScript = ""
var capturedArguments: [String] = []
_ = try await StdoutCapture.capture {
do {
try await ReactCommand.run(
values: values,
runtime: runtime,
appleScriptRunner: { source, arguments in
capturedScript = source
capturedArguments = arguments
appleScriptRunner: { _, _ in
#expect(Bool(false))
}
)
#expect(Bool(false))
} catch let error as IMsgError {
switch error {
case .unsupportedReaction(let message):
#expect(message.contains("custom emoji tapback"))
#expect(message.contains("AppleScript automation"))
#expect(message.contains("love"))
default:
#expect(Bool(false))
}
} catch {
#expect(Bool(false))
}
#expect(capturedArguments == ["iMessage;+;chat123", "Test Chat", "🎉"])
#expect(capturedScript.contains("on run argv"))
#expect(capturedScript.contains("keystroke customEmoji"))
#expect(capturedScript.contains("key code 36"))
#expect(capturedScript.contains("chat123") == false)
}