Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 39 additions & 36 deletions Sources/IMsgCore/MessageStore+Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,45 +286,48 @@ extension MessageStore {

return try withConnection { db in
let rows = try db.prepareRowIterator(query.sql, bindings: query.bindings)
guard let row = try rows.failableNext() else { return nil }
let decoded = try decodeMessageRow(
row,
columns: query.selection.columns,
fallbackChatID: query.fallbackChatID
)
let replyToGUID = replyToGUID(
associatedGuid: decoded.associatedGUID,
associatedType: decoded.associatedType
)
let threadOriginatorGUID =
decoded.threadOriginatorGUID.isEmpty ? nil : decoded.threadOriginatorGUID
var parentCache: ReplyParentCache = [:]
let parent = enrichedReplyContext(
db,
replyToGUID: replyToGUID,
threadOriginatorGUID: threadOriginatorGUID,
cache: &parentCache
)
return Message(
rowID: decoded.rowID,
chatID: decoded.chatID,
sender: decoded.sender,
text: decoded.text,
date: decoded.date,
isFromMe: decoded.isFromMe,
service: decoded.service,
handleID: decoded.handleID,
attachmentsCount: decoded.attachments,
guid: decoded.guid,
routing: Message.RoutingMetadata(
while let row = try rows.failableNext() {
let decoded = try decodeMessageRow(
row,
columns: query.selection.columns,
fallbackChatID: query.fallbackChatID
)
guard decoded.text == text else { continue }
let replyToGUID = replyToGUID(
associatedGuid: decoded.associatedGUID,
associatedType: decoded.associatedType
)
let threadOriginatorGUID =
decoded.threadOriginatorGUID.isEmpty ? nil : decoded.threadOriginatorGUID
var parentCache: ReplyParentCache = [:]
let parent = enrichedReplyContext(
db,
replyToGUID: replyToGUID,
threadOriginatorGUID: threadOriginatorGUID,
destinationCallerID: decoded.destinationCallerID.isEmpty
? nil : decoded.destinationCallerID,
replyToText: parent?.text,
replyToSender: parent?.sender
cache: &parentCache
)
)
return Message(
rowID: decoded.rowID,
chatID: decoded.chatID,
sender: decoded.sender,
text: decoded.text,
date: decoded.date,
isFromMe: decoded.isFromMe,
service: decoded.service,
handleID: decoded.handleID,
attachmentsCount: decoded.attachments,
guid: decoded.guid,
routing: Message.RoutingMetadata(
replyToGUID: replyToGUID,
threadOriginatorGUID: threadOriginatorGUID,
destinationCallerID: decoded.destinationCallerID.isEmpty
? nil : decoded.destinationCallerID,
replyToText: parent?.text,
replyToSender: parent?.sender
)
)
}
return nil
}
}

Expand Down
15 changes: 12 additions & 3 deletions Sources/IMsgCore/MessageStore+Queries.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,21 +107,30 @@ struct LatestSentMessageQuery {

init(store: MessageStore, text: String, chatID: ChatID?, since date: Date) {
self.selection = MessageRowSelection(store: store, includeChatID: true)
let bodyColumn = store.schema.hasAttributedBody ? "m.attributedBody" : "NULL"
var sql = """
SELECT \(selection.selectList)
FROM message m
LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
LEFT JOIN handle h ON m.handle_id = h.ROWID
WHERE m.is_from_me = 1
AND IFNULL(m.text, '') = ?
AND m.date >= ?
AND (
IFNULL(m.text, '') = ?
OR (IFNULL(m.text, '') = '' AND \(bodyColumn) IS NOT NULL)
)
"""
var bindings: [Binding?] = [text, MessageStore.appleEpoch(date)]
var bindings: [Binding?] = [MessageStore.appleEpoch(date), text]
if let chatID {
sql += " AND cmj.chat_id = ?"
bindings.append(chatID.rawValue)
}
sql += " ORDER BY m.date DESC, m.ROWID DESC LIMIT 1"
sql += """
ORDER BY CASE WHEN IFNULL(m.text, '') = ? THEN 0 ELSE 1 END,
m.date DESC, m.ROWID DESC
LIMIT 50
"""
bindings.append(text)

self.sql = sql
self.bindings = bindings
Expand Down
90 changes: 82 additions & 8 deletions Sources/IMsgHelper/IMsgInjected.m
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ - (NSArray *)activeAccounts;
@interface IMHandleRegistrar : NSObject
+ (instancetype)sharedInstance;
- (id)IMHandleWithID:(NSString *)handleID;
- (id)getIMHandlesForID:(NSString *)handleID;
@end

@interface IMChatRegistry : NSObject
Expand Down Expand Up @@ -392,6 +393,64 @@ - (id)currentIDStatusForDestination:(NSString *)destination service:(id)service;
};
}

static NSString *serviceNameForChat(IMChat *chat, NSString *chatGuid) {
NSString *serviceName = nil;
if ([chat respondsToSelector:@selector(account)]) {
id account = [chat performSelector:@selector(account)];
if ([account respondsToSelector:@selector(serviceName)]) {
serviceName = [account performSelector:@selector(serviceName)];
}
}
if (serviceName.length) return serviceName;
if ([chatGuid hasPrefix:@"SMS;"]) return @"SMS";
if ([chatGuid hasPrefix:@"iMessage;"]) return @"iMessage";
return nil;
}

static id firstHandleFromCandidate(id candidate, NSString *preferredService) {
NSMutableArray *handles = [NSMutableArray array];
if ([candidate isKindOfClass:[NSArray class]]) {
[handles addObjectsFromArray:candidate];
} else if ([candidate isKindOfClass:[NSSet class]]) {
[handles addObjectsFromArray:[candidate allObjects]];
} else if (candidate) {
[handles addObject:candidate];
}

for (id handle in handles) {
if (![handle respondsToSelector:@selector(serviceName)]) continue;
NSString *serviceName = [handle performSelector:@selector(serviceName)];
if ([serviceName isKindOfClass:[NSString class]] &&
[serviceName caseInsensitiveCompare:preferredService ?: @""] == NSOrderedSame) {
return handle;
}
}
return handles.firstObject;
}

static id vendIMHandle(id registrar, NSString *address, NSString *preferredService) {
if (!registrar || ![address isKindOfClass:[NSString class]] || address.length == 0) {
return nil;
}

@try {
if ([registrar respondsToSelector:@selector(IMHandleWithID:)]) {
id handle = [registrar performSelector:@selector(IMHandleWithID:)
withObject:address];
handle = firstHandleFromCandidate(handle, preferredService);
if (handle) return handle;
}
if ([registrar respondsToSelector:@selector(getIMHandlesForID:)]) {
id handles = [registrar performSelector:@selector(getIMHandlesForID:)
withObject:address];
return firstHandleFromCandidate(handles, preferredService);
}
} @catch (__unused NSException *ex) {
return nil;
}
return nil;
}

#pragma mark - Chat Resolution

static NSArray<NSString *>* chatIdentifierPrefixes(void) {
Expand Down Expand Up @@ -1786,11 +1845,14 @@ static void appendEvent(NSDictionary *evt) {

// Best-effort messageGuid; not always available immediately.
NSString *guid = lastSentMessageGuid(chat);
return successResponse(requestId, @{
NSMutableDictionary *response = [@{
@"chatGuid": chatGuid,
@"messageGuid": guid ?: @"",
@"queued": @(ddScan)
});
} mutableCopy];
NSString *serviceName = serviceNameForChat(chat, chatGuid);
if (serviceName.length) response[@"service"] = serviceName;
return successResponse(requestId, response);
} @catch (NSException *exception) {
return errorResponse(requestId,
[NSString stringWithFormat:@"send-message failed: %@", exception.reason]);
Expand Down Expand Up @@ -2845,16 +2907,26 @@ static void retargetPreparedTransfer(id ftc, IMFileTransfer *transfer,
NSArray *addresses = params[@"addresses"];
NSString *initialMessage = params[@"message"];
NSString *displayName = params[@"displayName"] ?: params[@"name"];
NSString *service = params[@"service"] ?: @"iMessage";
NSString *requestedService = params[@"service"] ?: @"iMessage";
NSString *preferredService = @"iMessage";
NSString *responseService = @"iMessage";

if (![addresses isKindOfClass:[NSArray class]] || addresses.count == 0) {
return errorResponse(requestId, @"Missing addresses array");
}
if ([service caseInsensitiveCompare:@"iMessage"] != NSOrderedSame) {
if ([requestedService caseInsensitiveCompare:@"iMessage"] == NSOrderedSame) {
preferredService = @"iMessage";
responseService = @"iMessage";
} else if ([requestedService caseInsensitiveCompare:@"auto"] == NSOrderedSame) {
preferredService = @"iMessage";
responseService = @"iMessage";
} else if ([requestedService caseInsensitiveCompare:@"sms"] == NSOrderedSame) {
preferredService = @"SMS";
responseService = @"SMS";
} else {
return errorResponse(requestId, [NSString stringWithFormat:
@"Unsupported chat-create service: %@", service]);
@"Unsupported chat-create service: %@", requestedService]);
}
service = @"iMessage";

Class hrClass = NSClassFromString(@"IMHandleRegistrar");
id hr = hrClass ? [hrClass performSelector:@selector(sharedInstance)] : nil;
Expand All @@ -2863,7 +2935,7 @@ static void retargetPreparedTransfer(id ftc, IMFileTransfer *transfer,
NSMutableArray *handles = [NSMutableArray array];
for (NSString *addr in addresses) {
if (![addr isKindOfClass:[NSString class]]) continue;
id h = [hr performSelector:@selector(IMHandleWithID:) withObject:addr];
id h = vendIMHandle(hr, addr, preferredService);
if (h) [handles addObject:h];
}
if (handles.count == 0) {
Expand Down Expand Up @@ -2901,9 +2973,11 @@ static void retargetPreparedTransfer(id ftc, IMFileTransfer *transfer,

NSString *guid = [chat respondsToSelector:@selector(guid)]
? [chat performSelector:@selector(guid)] : @"";
NSString *observedService = serviceNameForChat(chat, guid);
if (observedService.length) responseService = observedService;
return successResponse(requestId, @{
@"chatGuid": guid ?: @"",
@"service": service,
@"service": responseService,
@"messageGuid": messageGuid ?: @"",
@"participants": addresses
});
Expand Down
6 changes: 6 additions & 0 deletions Sources/imsg/RPCServer+ChatHandlers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ extension RPCServer {
if let guid = data["chatGuid"] as? String, !guid.isEmpty {
result["chat_guid"] = guid
}
if let messageGUID = data["messageGuid"] as? String, !messageGUID.isEmpty {
result["message_guid"] = messageGUID
}
if let service = data["service"] as? String, !service.isEmpty {
result["service"] = service
}
respond(id: id, result: result)
}

Expand Down
36 changes: 36 additions & 0 deletions Sources/imsg/RPCServer+Handlers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ extension RPCServer {
if let guid = data["messageGuid"] as? String, !guid.isEmpty {
result["guid"] = guid
}
if let chatGuid = data["chatGuid"] as? String, !chatGuid.isEmpty {
result["chat_guid"] = chatGuid
}
if let service = data["service"] as? String, !service.isEmpty {
result["service"] = service
}
respond(id: id, result: result)
return
} catch let err as RPCError {
Expand Down Expand Up @@ -281,6 +287,36 @@ extension RPCServer {
result["guid"] = sentMessage.guid
}
}
var responseChatInfo: ChatInfo?
if let sentMessage {
responseChatInfo = try? await cache.info(chatID: sentMessage.chatID)
}
if responseChatInfo == nil, let verificationChatID {
responseChatInfo = try? await cache.info(chatID: verificationChatID)
}
if responseChatInfo == nil {
responseChatInfo = directChatInfo
}
if let chatInfo = responseChatInfo {
if !chatInfo.guid.isEmpty {
result["chat_guid"] = chatInfo.guid
}
if !chatInfo.service.isEmpty {
result["service"] = chatInfo.service
}
}
if result["chat_guid"] == nil {
let resolvedChatGUID =
!resolvedTarget.chatGUID.isEmpty ? resolvedTarget.chatGUID : (directChatInfo?.guid ?? "")
if !resolvedChatGUID.isEmpty {
result["chat_guid"] = resolvedChatGUID
}
}
if result["service"] == nil,
let directService = directChatInfo?.service, !directService.isEmpty
{
result["service"] = directService
}
respond(id: id, result: result)
}

Expand Down
32 changes: 32 additions & 0 deletions Tests/IMsgCoreTests/MessageStoreSentMessageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,37 @@ func latestSentMessageFallsBackToNewestOutgoingTextWithoutChatFilter() throws {
#expect(message?.guid == "chat-two-guid")
}

@Test
func latestSentMessageMatchesAttributedBodyText() throws {
let db = try makeSentMessageDatabase()
let now = Date()
let text = "body fallback"
let bodyBytes = [UInt8(0x01), UInt8(0x2b)] + Array(text.utf8) + [0x86, 0x84]
try db.run(
"""
INSERT INTO message(
ROWID, handle_id, text, attributedBody, guid, associated_message_guid,
associated_message_type, date, is_from_me, service
)
VALUES (10, 1, NULL, ?, 'body-guid', NULL, 0, ?, 1, 'iMessage')
""",
Blob(bytes: bodyBytes),
TestDatabase.appleEpoch(now)
)
try db.run("INSERT INTO chat_message_join(chat_id, message_id) VALUES (1, 10)")
let store = try MessageStore(connection: db, path: ":memory:")

let message = try store.latestSentMessage(
matchingText: text,
chatID: 1,
since: now.addingTimeInterval(-1)
)

#expect(message?.rowID == 10)
#expect(message?.guid == "body-guid")
#expect(message?.text == text)
}

@Test
func chatInfoMatchingTargetHandlesAnyGroupPolarityMismatch() throws {
let db = try makeSentMessageDatabase()
Expand Down Expand Up @@ -143,6 +174,7 @@ private func makeSentMessageDatabase() throws -> Connection {
ROWID INTEGER PRIMARY KEY,
handle_id INTEGER,
text TEXT,
attributedBody BLOB,
guid TEXT,
associated_message_guid TEXT,
associated_message_type INTEGER,
Expand Down
6 changes: 5 additions & 1 deletion Tests/imsgTests/RPCServerBridgeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func rpcSendUsesBridgeWhenReadyAndExistingDirectChatResolves() async throws {
invokeBridge: { action, params in
capturedAction = action
capturedParams = params
return ["messageGuid": "bridge-guid"]
return ["messageGuid": "bridge-guid", "chatGuid": "iMessage;-;+123", "service": "iMessage"]
},
isBridgeReady: { true }
)
Expand All @@ -36,6 +36,8 @@ func rpcSendUsesBridgeWhenReadyAndExistingDirectChatResolves() async throws {
let result = output.responses.first?["result"] as? [String: Any]
#expect(result?["transport"] as? String == "bridge")
#expect(result?["guid"] as? String == "bridge-guid")
#expect(result?["chat_guid"] as? String == "iMessage;-;+123")
#expect(result?["service"] as? String == "iMessage")
}

@Test
Expand All @@ -60,6 +62,8 @@ func rpcSendFallsBackToAppleScriptWhenAutoBridgeFails() async throws {
#expect(captured?.recipient == "+123")
let result = output.responses.first?["result"] as? [String: Any]
#expect(result?["transport"] as? String == "applescript")
#expect(result?["chat_guid"] as? String == "iMessage;-;+123")
#expect(result?["service"] as? String == "iMessage")
}

@Test
Expand Down
Loading