imsg/Sources/IMsgCore/MessageStore+Search.swift
Omar Shahine c56c24d488
feat: port BlueBubbles private-API bridge
Port the BlueBubbles-inspired IMCore bridge surface into imsg with rich sends, message mutation, chat management, account/nickname introspection, live bridge events, and v2 UUID-keyed IPC.

Fixes #60.

Co-authored-by: Omar Shahine <omarshahine@users.noreply.github.com>
2026-05-06 06:28:00 +01:00

95 lines
3.0 KiB
Swift

import Foundation
import SQLite
private struct SearchMessagesQuery {
let sql: String
let bindings: [Binding?]
let selection: MessageRowSelection
let fallbackChatID: Int64? = nil
init(store: MessageStore, text: String, exact: Bool, limit: Int) {
self.selection = MessageRowSelection(store: store, includeChatID: true)
let reactionFilter =
store.schema.hasReactionColumns
? " AND (m.associated_message_type IS NULL OR m.associated_message_type < 2000 OR m.associated_message_type > 3006)"
: ""
let predicate =
exact
? "IFNULL(m.text, '') = ? COLLATE NOCASE"
: "IFNULL(m.text, '') LIKE ? ESCAPE '\\' COLLATE NOCASE"
let textBinding = exact ? text : SearchMessagesQuery.likePattern(for: text)
self.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 \(predicate)\(reactionFilter)
ORDER BY m.date DESC, m.ROWID DESC
LIMIT ?
"""
self.bindings = [textBinding, limit]
}
private static func likePattern(for text: String) -> String {
var escaped = ""
for char in text {
if char == "\\" || char == "%" || char == "_" {
escaped.append("\\")
}
escaped.append(char)
}
return "%\(escaped)%"
}
}
extension MessageStore {
public func searchMessages(query text: String, match: String, limit: Int) throws -> [Message] {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return [] }
let exact = match.lowercased() == "exact"
let query = SearchMessagesQuery(
store: self,
text: trimmed,
exact: exact,
limit: limit
)
return try withConnection { db in
var messages: [Message] = []
let rows = try db.prepareRowIterator(query.sql, bindings: query.bindings)
while let row = try rows.failableNext() {
let decoded = try decodeMessageRow(
row,
columns: query.selection.columns,
fallbackChatID: query.fallbackChatID
)
let replyToGUID = replyToGUID(
associatedGuid: decoded.associatedGUID,
associatedType: decoded.associatedType
)
messages.append(
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: decoded.threadOriginatorGUID.isEmpty
? nil : decoded.threadOriginatorGUID,
destinationCallerID: decoded.destinationCallerID.isEmpty
? nil : decoded.destinationCallerID
)
))
}
return messages
}
}
}