Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Sources/ICloudCLICore/CommandLine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1343,7 +1343,7 @@ Usage:
icloud-cli findmy devices|people [--include-coordinates] [--format json|text] [--findmy-store PATH]
icloud-cli mail accounts|mailboxes [--account NAME] [--format json|text] [--mail-store PATH]
icloud-cli mail recent [--confirm-sensitive] [--account NAME] [--mailbox NAME] [--limit N] [--format json|text] [--mail-store PATH]
icloud-cli messages conversations [--format json|text] [--chat-db PATH]
icloud-cli messages conversations [--limit N] [--format json|text] [--chat-db PATH]
icloud-cli messages recent [--confirm-sensitive] [--include-body] [--since ISO8601] [--limit N] [--format json|text] [--chat-db PATH]
icloud-cli maps favorites [--format json|text] [--maps-store PATH]
icloud-cli maps recents [--limit N] [--format json|text] [--maps-store PATH]
Expand Down
2 changes: 1 addition & 1 deletion Sources/ICloudCLICore/CommandRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public struct CommandRunner: Sendable {
output(try render(places, format: options.format))
return 0
case .messagesConversations(let options):
let conversations = try LocalSQLiteInventoryReader(database: options.chatDatabase).messageConversations()
let conversations = try LocalSQLiteInventoryReader(database: options.chatDatabase).messageConversations(limit: options.limit)
output(try render(conversations, format: options.format))
return 0
case .messagesRecent(let options):
Expand Down
7 changes: 4 additions & 3 deletions Sources/ICloudCLICore/LocalInventories.swift
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,9 @@ public struct LocalSQLiteInventoryReader: Sendable {
return rows.map { SafariHistoryEntry(url: redactURL($0.url), title: $0.title, visitedAt: $0.visitedAt, visitCount: $0.visitCount) }
}

public func messageConversations() throws -> [MessageConversation] {
public func messageConversations(limit: Int = 50) throws -> [MessageConversation] {
if try tableExists("message_conversations") {
return try query("SELECT chatIdentifier, displayName, participantCount, lastMessageAt, messageCount FROM message_conversations ORDER BY lastMessageAt DESC;")
return try query("SELECT chatIdentifier, displayName, participantCount, lastMessageAt, messageCount FROM message_conversations ORDER BY lastMessageAt DESC LIMIT \(bounded(limit, defaultValue: 50, max: 1000));")
}
guard try tableExists("chat"), try tableExists("message"), try tableExists("chat_message_join") else {
throw LocalInventoryError.unsupportedSchema(store: database.path, detail: "missing message_conversations or chat/message/chat_message_join tables")
Expand All @@ -348,7 +348,8 @@ public struct LocalSQLiteInventoryReader: Sendable {
LEFT JOIN message m ON m.ROWID = cmj.message_id
LEFT JOIN chat_handle_join chj ON chj.chat_id = c.ROWID
GROUP BY c.ROWID
ORDER BY MAX(m.date) DESC;
ORDER BY MAX(m.date) DESC
LIMIT \(bounded(limit, defaultValue: 50, max: 1000));
""")
}

Expand Down
30 changes: 30 additions & 0 deletions Tests/ICloudCLICoreTests/LocalInventoriesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ import Testing
#expect(safariOptions.confirmSensitive == true)
#expect(safariOptions.redactURLs == true)

let messages = try CLIParser().parse(arguments: ["icloud-cli", "messages", "conversations", "--limit", "5", "--chat-db", "/tmp/chat.db"])
guard case .messagesConversations(let messagesOptions) = messages else {
Issue.record("Expected messages conversations command")
return
}
#expect(messagesOptions.limit == 5)
#expect(messagesOptions.chatDatabase.path == "/tmp/chat.db")

let watch = try CLIParser().parse(arguments: ["icloud-cli", "watch", "--once", "--commands", "safari-tabs,storage-status", "--output-dir", "/tmp/cache"])
guard case .watch(let watchOptions) = watch else {
Issue.record("Expected watch command")
Expand Down Expand Up @@ -100,6 +108,16 @@ import Testing
#expect(recent.first?.body == nil)
}

@Test func limitsMessageConversations() throws {
let database = try messageConversationsFixtureDatabase()
defer { try? FileManager.default.removeItem(at: database.deletingLastPathComponent()) }
let reader = LocalSQLiteInventoryReader(database: database)

let conversations = try reader.messageConversations(limit: 1)

#expect(conversations.map(\.chatIdentifier) == ["chat-new"])
}

@Test func readsAppleAddressBookRecordSchema() throws {
let database = try appleAddressBookFixtureDatabase()
defer { try? FileManager.default.removeItem(at: database.deletingLastPathComponent()) }
Expand Down Expand Up @@ -183,6 +201,18 @@ private func appleMessagesFixtureDatabase() throws -> URL {
return database
}

private func messageConversationsFixtureDatabase() throws -> URL {
let root = try temporaryDirectory(named: "message-conversations")
let database = root.appendingPathComponent("inventory.sqlite")
let sql = """
CREATE TABLE message_conversations (chatIdentifier TEXT, displayName TEXT, participantCount INTEGER, lastMessageAt TEXT, messageCount INTEGER);
INSERT INTO message_conversations VALUES ('chat-old', 'Old Chat', 1, '2026-01-01T00:00:00Z', 1);
INSERT INTO message_conversations VALUES ('chat-new', 'New Chat', 1, '2026-01-02T00:00:00Z', 1);
"""
try runSQLite(database: database, sql: sql)
return database
}

private func appleAddressBookFixtureDatabase() throws -> URL {
let root = try temporaryDirectory(named: "apple-addressbook")
let database = root.appendingPathComponent("AddressBook-v22.abcddb")
Expand Down
Loading