diff --git a/CHANGELOG.md b/CHANGELOG.md index e328fcb..a5abdcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index c5a45be..eb1b568 100644 --- a/README.md +++ b/README.md @@ -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 aren’t copied. diff --git a/Sources/IMsgCore/Errors.swift b/Sources/IMsgCore/Errors.swift index 6508150..7a9c762 100644 --- a/Sources/IMsgCore/Errors.swift +++ b/Sources/IMsgCore/Errors.swift @@ -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)" } diff --git a/Sources/imsg/Commands/ReactCommand.swift b/Sources/imsg/Commands/ReactCommand.swift index ec9c138..0765e43 100644 --- a/Sources/imsg/Commands/ReactCommand.swift +++ b/Sources/imsg/Commands/ReactCommand.swift @@ -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 = """ diff --git a/Tests/imsgTests/ReactCommandTests.swift b/Tests/imsgTests/ReactCommandTests.swift index 04f3fa1..043fd7b 100644 --- a/Tests/imsgTests/ReactCommandTests.swift +++ b/Tests/imsgTests/ReactCommandTests.swift @@ -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) }