diff --git a/Sources/IMsgCore/MessageStore+Accounts.swift b/Sources/IMsgCore/MessageStore+Accounts.swift index 59427079..0fe126fb 100644 --- a/Sources/IMsgCore/MessageStore+Accounts.swift +++ b/Sources/IMsgCore/MessageStore+Accounts.swift @@ -5,23 +5,26 @@ import SQLite public struct LocalAccount: Sendable, Equatable { /// The account login (typically your Apple ID email or phone), from `chat.account_login`. public let login: String - /// The full account identifier (e.g. `iMessage;+;you@icloud.com`), from `chat.account_id`. + /// The opaque account identifier from `chat.account_id`. public let accountID: String + /// The messaging service from `chat.service_name` (for example, `iMessage` or `SMS`). + public let service: String /// Number of local chats routed through this account (useful to rank the primary account). public let chatCount: Int - public init(login: String, accountID: String, chatCount: Int) { + public init(login: String, accountID: String, chatCount: Int, service: String = "") { self.login = login self.accountID = accountID self.chatCount = chatCount + self.service = service } } extension MessageStore { /// Enumerate the messaging accounts seen in the local `chat.db`. /// - /// This reads `chat.account_login` / `chat.account_id` recorded on each chat, - /// grouped and ranked by how many chats use them. It reflects accounts + /// This reads `chat.account_login`, `chat.account_id`, and `chat.service_name` + /// recorded on each chat, grouped and ranked by how many chats use them. It reflects accounts /// *observed in history*, not a live "currently signed-in" list — for a /// single-account Mac these are equivalent, and the result is factual rather /// than inferred. Returns an empty array on schemas without account columns. @@ -34,9 +37,10 @@ extension MessageStore { let sql = """ SELECT \(loginColumn) AS account_login, \(accountIDColumn) AS account_id, + IFNULL(c.service_name, '') AS service_name, COUNT(*) AS chat_count FROM chat c - GROUP BY account_login, account_id + GROUP BY account_login, account_id, service_name HAVING account_login != '' OR account_id != '' ORDER BY chat_count DESC """ @@ -46,9 +50,10 @@ extension MessageStore { while let row = try rows.failableNext() { let login = try stringValue(row, "account_login") let accountID = try stringValue(row, "account_id") + let service = try stringValue(row, "service_name") let count = try intValue(row, "chat_count") ?? 0 accounts.append( - LocalAccount(login: login, accountID: accountID, chatCount: count) + LocalAccount(login: login, accountID: accountID, chatCount: count, service: service) ) } return accounts diff --git a/Tests/IMsgCoreTests/MessageStoreAccountsTests.swift b/Tests/IMsgCoreTests/MessageStoreAccountsTests.swift new file mode 100644 index 00000000..742dde9d --- /dev/null +++ b/Tests/IMsgCoreTests/MessageStoreAccountsTests.swift @@ -0,0 +1,34 @@ +import SQLite +import Testing + +@testable import IMsgCore + +@Test +func localAccountsPreservesOpaqueAccountIDAndReportsChatService() throws { + let db = try Connection(.inMemory) + try MessageDatabaseFixture.createSchema(db) + try db.run( + """ + INSERT INTO chat( + ROWID, chat_identifier, guid, service_name, account_id, account_login + ) + VALUES + (1, '+111', 'iMessage;-;+111', 'iMessage', + '9E876D56-1542-4DB5-A437-A4052D038DB9', 'E:me@example.com'), + (2, '+222', 'SMS;-;+222', 'SMS', + '34CD7B53-36C2-48ED-8FC3-7226D8BC538A', 'P:+15555550123'), + (3, '+333', 'iMessage;-;+333', 'iMessage', + '9E876D56-1542-4DB5-A437-A4052D038DB9', 'E:me@example.com') + """ + ) + let store = try MessageStore(connection: db, path: ":memory:") + + let accounts = try store.localAccounts() + + #expect(accounts.map(\.service) == ["iMessage", "SMS"]) + #expect( + accounts.map(\.accountID) == [ + "9E876D56-1542-4DB5-A437-A4052D038DB9", + "34CD7B53-36C2-48ED-8FC3-7226D8BC538A", + ]) +}