diff --git a/CHANGELOG.md b/CHANGELOG.md index 38611973..a112e81a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 0.13.2 - Unreleased +### Advanced IMCore +- fix: restore group participant add/remove on macOS 26 by using fallback-capable handle lookup and probing both current and legacy IMChat selectors (#185, thanks @oficiallyAkshay). + ## 0.13.1 - 2026-07-17 ### Highlights diff --git a/Sources/IMsgHelper/IMsgInjected.m b/Sources/IMsgHelper/IMsgInjected.m index 5728db9b..e863ca33 100644 --- a/Sources/IMsgHelper/IMsgInjected.m +++ b/Sources/IMsgHelper/IMsgInjected.m @@ -5775,19 +5775,20 @@ static void cleanupPreparedStickerPaths(NSString *snapshotPath, Class hrClass = NSClassFromString(@"IMHandleRegistrar"); id hr = hrClass ? [hrClass performSelector:@selector(sharedInstance)] : nil; - id handle = (hr && [hr respondsToSelector:@selector(IMHandleWithID:)]) - ? [hr performSelector:@selector(IMHandleWithID:) withObject:address] - : nil; + // Match chat-create's fallback-capable iMessage handle lookup. + id handle = vendIMHandle(hr, address, @"iMessage", YES); if (!handle) return errorResponse(requestId, @"Could not vend handle"); @try { - // BB-verified macOS 11+ selector: `inviteParticipantsToiMessageChat:reason:`. - // `addParticipantsToiMessageChat:reason:` (what we used before) is not - // declared on IMChat; respondsToSelector returned NO and the call - // failed with "selector not available". - SEL sel = @selector(inviteParticipantsToiMessageChat:reason:); - if (![chat respondsToSelector:sel]) { - return errorResponse(requestId, @"inviteParticipantsToiMessageChat:reason: not available"); + // macOS 26 renamed this selector; retain the older spelling as fallback. + SEL sel = 0; + for (NSString *name in @[@"inviteParticipants:reason:", + @"inviteParticipantsToiMessageChat:reason:"]) { + SEL cand = NSSelectorFromString(name); + if ([chat respondsToSelector:cand]) { sel = cand; break; } + } + if (!sel) { + return errorResponse(requestId, @"no participant-invite selector available on this IMChat"); } NSMethodSignature *sig = [chat methodSignatureForSelector:sel]; NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig]; @@ -5823,9 +5824,15 @@ static void cleanupPreparedStickerPaths(NSString *snapshotPath, if (!targetHandle) return errorResponse(requestId, @"Participant not found on chat"); @try { - SEL sel = @selector(removeParticipantsFromiMessageChat:reason:); - if (![chat respondsToSelector:sel]) { - return errorResponse(requestId, @"removeParticipantsFromiMessageChat:reason: not available"); + // macOS 26 renamed this selector; retain the older spelling as fallback. + SEL sel = 0; + for (NSString *name in @[@"removeParticipants:reason:", + @"removeParticipantsFromiMessageChat:reason:"]) { + SEL cand = NSSelectorFromString(name); + if ([chat respondsToSelector:cand]) { sel = cand; break; } + } + if (!sel) { + return errorResponse(requestId, @"no participant-remove selector available on this IMChat"); } NSMethodSignature *sig = [chat methodSignatureForSelector:sel]; NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig]; diff --git a/Tests/imsgTests/BridgeParticipantMutationTests.swift b/Tests/imsgTests/BridgeParticipantMutationTests.swift new file mode 100644 index 00000000..41a9b60a --- /dev/null +++ b/Tests/imsgTests/BridgeParticipantMutationTests.swift @@ -0,0 +1,52 @@ +import Foundation +import Testing + +@Test +func participantMutationsUseCurrentAndLegacySelectors() throws { + let source = try participantMutationBridgeSource() + let addBody = try #require( + participantMutationFunctionBody(named: "handleAddParticipant", in: source)) + let removeBody = try #require( + participantMutationFunctionBody(named: "handleRemoveParticipant", in: source)) + + #expect(addBody.contains(#"vendIMHandle(hr, address, @"iMessage", YES)"#)) + #expect(addBody.contains(#"@"inviteParticipants:reason:""#)) + #expect(addBody.contains(#"@"inviteParticipantsToiMessageChat:reason:""#)) + #expect(removeBody.contains(#"@"removeParticipants:reason:""#)) + #expect(removeBody.contains(#"@"removeParticipantsFromiMessageChat:reason:""#)) +} + +private func participantMutationBridgeSource() throws -> String { + let testFile = URL(fileURLWithPath: #filePath) + let repoRoot = + testFile + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + let helper = repoRoot.appendingPathComponent("Sources/IMsgHelper/IMsgInjected.m") + return try String(contentsOf: helper, encoding: .utf8) +} + +private func participantMutationFunctionBody(named name: String, in source: String) -> String? { + var searchStart = source.startIndex + while let nameRange = source.range(of: name, range: searchStart..