From f1279eb2fbf5c9a5a04ce76b88706ec72f36a39a Mon Sep 17 00:00:00 2001 From: Pheidon Date: Thu, 11 Jun 2026 22:06:15 +0000 Subject: [PATCH] Limit message conversation output --- Sources/ICloudCLICore/CommandLine.swift | 2 +- Sources/ICloudCLICore/CommandRunner.swift | 2 +- Sources/ICloudCLICore/LocalInventories.swift | 7 +++-- .../LocalInventoriesTests.swift | 30 +++++++++++++++++++ 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/Sources/ICloudCLICore/CommandLine.swift b/Sources/ICloudCLICore/CommandLine.swift index 65c4d36..f7df2f5 100644 --- a/Sources/ICloudCLICore/CommandLine.swift +++ b/Sources/ICloudCLICore/CommandLine.swift @@ -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] diff --git a/Sources/ICloudCLICore/CommandRunner.swift b/Sources/ICloudCLICore/CommandRunner.swift index f3f6d5b..50cc171 100644 --- a/Sources/ICloudCLICore/CommandRunner.swift +++ b/Sources/ICloudCLICore/CommandRunner.swift @@ -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): diff --git a/Sources/ICloudCLICore/LocalInventories.swift b/Sources/ICloudCLICore/LocalInventories.swift index 8f94f1c..b6b8ed6 100644 --- a/Sources/ICloudCLICore/LocalInventories.swift +++ b/Sources/ICloudCLICore/LocalInventories.swift @@ -281,9 +281,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") @@ -300,7 +300,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)); """) } diff --git a/Tests/ICloudCLICoreTests/LocalInventoriesTests.swift b/Tests/ICloudCLICoreTests/LocalInventoriesTests.swift index 597ceeb..4cff3b7 100644 --- a/Tests/ICloudCLICoreTests/LocalInventoriesTests.swift +++ b/Tests/ICloudCLICoreTests/LocalInventoriesTests.swift @@ -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") @@ -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 resolvesAddressBookDatabaseInsideSourcesDirectory() throws { let root = try temporaryDirectory(named: "addressbook") defer { try? FileManager.default.removeItem(at: root) } @@ -146,6 +164,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 syntheticInventoryDatabase() throws -> URL { let root = try temporaryDirectory(named: "sqlite") let database = root.appendingPathComponent("inventory.sqlite")