imsg/Sources/IMsgCore/MessageStore+SentMessages.swift
2026-05-05 03:38:10 +01:00

124 lines
4.4 KiB
Swift

import Foundation
import SQLite
extension MessageStore {
public func chatInfo(matchingTarget target: String) throws -> ChatInfo? {
let candidates = Self.chatTargetCandidates(target)
guard !candidates.isEmpty else { return nil }
let placeholders = Array(repeating: "?", count: candidates.count).joined(separator: ",")
let accountIDColumn = schema.hasChatAccountIDColumn ? "IFNULL(c.account_id, '')" : "''"
let accountLoginColumn = schema.hasChatAccountLoginColumn ? "IFNULL(c.account_login, '')" : "''"
let lastAddressedHandleColumn =
schema.hasChatLastAddressedHandleColumn ? "IFNULL(c.last_addressed_handle, '')" : "''"
let sql = """
SELECT c.ROWID AS chat_rowid, IFNULL(c.chat_identifier, '') AS identifier, IFNULL(c.guid, '') AS guid,
IFNULL(c.display_name, c.chat_identifier) AS name, IFNULL(c.service_name, '') AS service,
\(accountIDColumn) AS account_id,
\(accountLoginColumn) AS account_login,
\(lastAddressedHandleColumn) AS last_addressed_handle
FROM chat c
WHERE c.chat_identifier IN (\(placeholders))
OR c.guid IN (\(placeholders))
LIMIT 1
"""
let bindings: [Binding?] = candidates + candidates
return try withConnection { db in
let rows = try db.prepareRowIterator(sql, bindings: bindings)
guard let row = try rows.failableNext() else { return nil }
return ChatInfo(
id: try int64Value(row, "chat_rowid") ?? 0,
identifier: try stringValue(row, "identifier"),
guid: try stringValue(row, "guid"),
name: try stringValue(row, "name"),
service: try stringValue(row, "service"),
accountID: try stringValue(row, "account_id").nilIfEmpty,
accountLogin: try stringValue(row, "account_login").nilIfEmpty,
lastAddressedHandle: try stringValue(row, "last_addressed_handle").nilIfEmpty
)
}
}
public func latestUnjoinedSentMessageRowID(
matchingTargetHandles handles: [String],
since date: Date
) throws -> Int64? {
let candidates = Self.chatTargetHandleCandidates(handles)
guard !candidates.isEmpty else { return nil }
let placeholders = Array(repeating: "?", count: candidates.count).joined(separator: ",")
let sql = """
SELECT m.ROWID AS message_rowid
FROM message m
LEFT JOIN chat_message_join cmj ON cmj.message_id = m.ROWID
LEFT JOIN handle h ON h.ROWID = m.handle_id
WHERE m.is_from_me = 1
AND m.date >= ?
AND IFNULL(m.text, '') = ''
AND cmj.message_id IS NULL
AND IFNULL(h.id, '') IN (\(placeholders))
ORDER BY m.date DESC, m.ROWID DESC
LIMIT 1
"""
let bindings: [Binding?] = [MessageStore.appleEpoch(date)] + candidates
return try withConnection { db in
let rows = try db.prepareRowIterator(sql, bindings: bindings)
guard let row = try rows.failableNext() else { return nil }
return try int64Value(row, "message_rowid")
}
}
private static func chatTargetHandleCandidates(_ handles: [String]) -> [String] {
var candidates: [String] = []
for handle in handles {
candidates.append(contentsOf: chatTargetCandidates(handle))
}
return dedupe(candidates)
}
private static func chatTargetCandidates(_ target: String) -> [String] {
let trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return [] }
var candidates = [trimmed]
if let toggled = toggledAnyGroupPolarity(trimmed) {
candidates.append(toggled)
}
if let bare = bareAnyGroupIdentifier(trimmed) {
candidates.append(bare)
}
return dedupe(candidates)
}
private static func toggledAnyGroupPolarity(_ value: String) -> String? {
if value.hasPrefix("any;+;") {
return "any;-;" + value.dropFirst("any;+;".count)
}
if value.hasPrefix("any;-;") {
return "any;+;" + value.dropFirst("any;-;".count)
}
return nil
}
private static func bareAnyGroupIdentifier(_ value: String) -> String? {
if value.hasPrefix("any;+;") {
return String(value.dropFirst("any;+;".count))
}
if value.hasPrefix("any;-;") {
return String(value.dropFirst("any;-;".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
}
}