fix: normalize typing chat lookup
This commit is contained in:
parent
6253bdd135
commit
f8f0c5d712
@ -19,9 +19,8 @@
|
||||
- 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: remove non-functional `typing` command and related RPC methods
|
||||
- fix: remove unsupported standalone IMCore typing path and stale error branch
|
||||
- test: drop typing-specific unit/integration tests with command/RPC surface removal
|
||||
- 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)
|
||||
|
||||
## 0.5.0 - 2026-02-16
|
||||
|
||||
|
||||
@ -132,6 +132,9 @@ Important:
|
||||
- This is opt-in only. Default send/history/watch flows do not need injection.
|
||||
- `imsg launch` refuses to inject when SIP is enabled.
|
||||
- `imsg status` is read-only and does not auto-launch or auto-inject.
|
||||
- macOS 26 can also block Messages.app dylib injection with library validation.
|
||||
In that case `imsg status` reports advanced features unavailable even with SIP
|
||||
disabled; normal send/history/watch commands still work.
|
||||
|
||||
Setup:
|
||||
1) Disable SIP from Recovery mode: `csrutil disable`
|
||||
|
||||
@ -197,19 +197,27 @@ public struct TypingIndicator: Sendable {
|
||||
throw IMsgError.typingIndicatorFailed("Failed to get IMChatRegistry shared instance")
|
||||
}
|
||||
|
||||
let candidates = chatLookupCandidates(for: identifier)
|
||||
|
||||
let guidSel = sel_registerName("existingChatWithGUID:")
|
||||
if registry.responds(to: guidSel) {
|
||||
if let chat = registry.perform(guidSel, with: identifier)?.takeUnretainedValue() as? NSObject
|
||||
{
|
||||
return chat
|
||||
for candidate in candidates {
|
||||
if let chat = registry.perform(guidSel, with: candidate)?.takeUnretainedValue()
|
||||
as? NSObject
|
||||
{
|
||||
return chat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let identSel = sel_registerName("existingChatWithChatIdentifier:")
|
||||
if registry.responds(to: identSel) {
|
||||
if let chat = registry.perform(identSel, with: identifier)?.takeUnretainedValue() as? NSObject
|
||||
{
|
||||
return chat
|
||||
for candidate in candidates {
|
||||
if let chat = registry.perform(identSel, with: candidate)?.takeUnretainedValue()
|
||||
as? NSObject
|
||||
{
|
||||
return chat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -217,6 +225,48 @@ public struct TypingIndicator: Sendable {
|
||||
"Chat not found for identifier: \(identifier). "
|
||||
+ "Make sure Messages.app has an active conversation with this contact.")
|
||||
}
|
||||
|
||||
static func chatLookupCandidates(for identifier: String) -> [String] {
|
||||
let trimmed = identifier.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return [] }
|
||||
|
||||
let bareIdentifier = stripKnownChatPrefix(trimmed) ?? trimmed
|
||||
var candidates = [trimmed]
|
||||
if bareIdentifier != trimmed {
|
||||
candidates.append(bareIdentifier)
|
||||
}
|
||||
for prefix in chatIdentifierPrefixes {
|
||||
candidates.append(prefix + bareIdentifier)
|
||||
}
|
||||
return dedupe(candidates)
|
||||
}
|
||||
|
||||
private static let chatIdentifierPrefixes = [
|
||||
"iMessage;-;",
|
||||
"iMessage;+;",
|
||||
"SMS;-;",
|
||||
"SMS;+;",
|
||||
"any;-;",
|
||||
"any;+;",
|
||||
]
|
||||
|
||||
private static func stripKnownChatPrefix(_ value: String) -> String? {
|
||||
for prefix in chatIdentifierPrefixes where value.hasPrefix(prefix) {
|
||||
return String(value.dropFirst(prefix.count))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func dedupe(_ values: [String]) -> [String] {
|
||||
var seen = Set<String>()
|
||||
var result: [String] = []
|
||||
for value in values where !value.isEmpty {
|
||||
if seen.insert(value).inserted {
|
||||
result.append(value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private final class DaemonConnectionTracker: @unchecked Sendable {
|
||||
|
||||
@ -75,6 +75,19 @@ static NSDictionary* errorResponse(NSInteger requestId, NSString *error) {
|
||||
|
||||
#pragma mark - Chat Resolution
|
||||
|
||||
static NSArray<NSString *>* chatIdentifierPrefixes(void) {
|
||||
return @[@"iMessage;-;", @"iMessage;+;", @"SMS;-;", @"SMS;+;", @"any;-;", @"any;+;"];
|
||||
}
|
||||
|
||||
static NSString* stripKnownChatPrefix(NSString *value) {
|
||||
for (NSString *prefix in chatIdentifierPrefixes()) {
|
||||
if ([value hasPrefix:prefix]) {
|
||||
return [value substringFromIndex:prefix.length];
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
/// Try multiple methods to find a chat, including GUID lookup, chat identifier,
|
||||
/// and participant matching with phone number normalization.
|
||||
static id findChat(NSString *identifier) {
|
||||
@ -91,6 +104,7 @@ static id findChat(NSString *identifier) {
|
||||
}
|
||||
|
||||
id chat = nil;
|
||||
NSString *bareIdentifier = stripKnownChatPrefix(identifier) ?: identifier;
|
||||
|
||||
// Method 1: Try existingChatWithGUID: with the identifier as-is (if it looks like a GUID)
|
||||
SEL guidSel = @selector(existingChatWithGUID:);
|
||||
@ -104,9 +118,8 @@ static id findChat(NSString *identifier) {
|
||||
}
|
||||
|
||||
// Try constructing GUIDs with common prefixes (iMessage, SMS, any)
|
||||
NSArray *prefixes = @[@"iMessage;-;", @"iMessage;+;", @"SMS;-;", @"SMS;+;", @"any;-;", @"any;+;"];
|
||||
for (NSString *prefix in prefixes) {
|
||||
NSString *fullGUID = [prefix stringByAppendingString:identifier];
|
||||
for (NSString *prefix in chatIdentifierPrefixes()) {
|
||||
NSString *fullGUID = [prefix stringByAppendingString:bareIdentifier];
|
||||
chat = [registry performSelector:guidSel withObject:fullGUID];
|
||||
if (chat) {
|
||||
NSLog(@"[imsg-bridge] Found chat via existingChatWithGUID: %@", fullGUID);
|
||||
@ -123,6 +136,13 @@ static id findChat(NSString *identifier) {
|
||||
NSLog(@"[imsg-bridge] Found chat via existingChatWithChatIdentifier: %@", identifier);
|
||||
return chat;
|
||||
}
|
||||
if (![bareIdentifier isEqualToString:identifier]) {
|
||||
chat = [registry performSelector:identSel withObject:bareIdentifier];
|
||||
if (chat) {
|
||||
NSLog(@"[imsg-bridge] Found chat via existingChatWithChatIdentifier: %@", bareIdentifier);
|
||||
return chat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Iterate all chats and match by participant
|
||||
@ -138,12 +158,13 @@ static id findChat(NSString *identifier) {
|
||||
|
||||
// Normalize the search identifier for phone number matching
|
||||
NSString *normalizedIdentifier = nil;
|
||||
if ([identifier hasPrefix:@"+"] || [identifier hasPrefix:@"1"] ||
|
||||
if (bareIdentifier.length > 0 &&
|
||||
([bareIdentifier hasPrefix:@"+"] || [bareIdentifier hasPrefix:@"1"] ||
|
||||
[[NSCharacterSet decimalDigitCharacterSet]
|
||||
characterIsMember:[identifier characterAtIndex:0]]) {
|
||||
characterIsMember:[bareIdentifier characterAtIndex:0]])) {
|
||||
NSMutableString *digits = [NSMutableString string];
|
||||
for (NSUInteger i = 0; i < identifier.length; i++) {
|
||||
unichar c = [identifier characterAtIndex:i];
|
||||
for (NSUInteger i = 0; i < bareIdentifier.length; i++) {
|
||||
unichar c = [bareIdentifier characterAtIndex:i];
|
||||
if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:c]) {
|
||||
[digits appendFormat:@"%C", c];
|
||||
}
|
||||
@ -155,7 +176,8 @@ static id findChat(NSString *identifier) {
|
||||
// Check GUID
|
||||
if ([aChat respondsToSelector:@selector(guid)]) {
|
||||
NSString *chatGUID = [aChat performSelector:@selector(guid)];
|
||||
if ([chatGUID isEqualToString:identifier]) {
|
||||
if ([chatGUID isEqualToString:identifier] ||
|
||||
[chatGUID isEqualToString:bareIdentifier]) {
|
||||
NSLog(@"[imsg-bridge] Found chat by GUID exact match: %@", chatGUID);
|
||||
return aChat;
|
||||
}
|
||||
@ -164,7 +186,8 @@ static id findChat(NSString *identifier) {
|
||||
// Check chatIdentifier
|
||||
if ([aChat respondsToSelector:@selector(chatIdentifier)]) {
|
||||
NSString *chatId = [aChat performSelector:@selector(chatIdentifier)];
|
||||
if ([chatId isEqualToString:identifier]) {
|
||||
if ([chatId isEqualToString:identifier] ||
|
||||
[chatId isEqualToString:bareIdentifier]) {
|
||||
NSLog(@"[imsg-bridge] Found chat by chatIdentifier exact match: %@", chatId);
|
||||
return aChat;
|
||||
}
|
||||
@ -177,7 +200,8 @@ static id findChat(NSString *identifier) {
|
||||
for (id handle in participants) {
|
||||
if ([handle respondsToSelector:@selector(ID)]) {
|
||||
NSString *handleID = [handle performSelector:@selector(ID)];
|
||||
if ([handleID isEqualToString:identifier]) {
|
||||
if ([handleID isEqualToString:identifier] ||
|
||||
[handleID isEqualToString:bareIdentifier]) {
|
||||
NSLog(@"[imsg-bridge] Found chat by participant exact match: %@", handleID);
|
||||
return aChat;
|
||||
}
|
||||
|
||||
@ -41,3 +41,40 @@ func typingIndicatorStopsAfterNormalDuration() async throws {
|
||||
#expect(didSleep == true)
|
||||
#expect(events == ["start", "stop"])
|
||||
}
|
||||
|
||||
@Test
|
||||
func typingLookupCandidatesExpandAnyPrefixToServiceVariants() {
|
||||
let candidates = TypingIndicator.chatLookupCandidates(for: "any;-;+15551234567")
|
||||
|
||||
#expect(
|
||||
candidates == [
|
||||
"any;-;+15551234567",
|
||||
"+15551234567",
|
||||
"iMessage;-;+15551234567",
|
||||
"iMessage;+;+15551234567",
|
||||
"SMS;-;+15551234567",
|
||||
"SMS;+;+15551234567",
|
||||
"any;+;+15551234567",
|
||||
])
|
||||
}
|
||||
|
||||
@Test
|
||||
func typingLookupCandidatesAvoidDoublePrefixingDirectIdentifiers() {
|
||||
let candidates = TypingIndicator.chatLookupCandidates(for: " iMessage;-;user@example.com ")
|
||||
|
||||
#expect(
|
||||
candidates == [
|
||||
"iMessage;-;user@example.com",
|
||||
"user@example.com",
|
||||
"iMessage;+;user@example.com",
|
||||
"SMS;-;user@example.com",
|
||||
"SMS;+;user@example.com",
|
||||
"any;-;user@example.com",
|
||||
"any;+;user@example.com",
|
||||
])
|
||||
}
|
||||
|
||||
@Test
|
||||
func typingLookupCandidatesRejectBlankIdentifier() {
|
||||
#expect(TypingIndicator.chatLookupCandidates(for: " ").isEmpty)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user