UNPKG

imessage-parser

Version:

Parse iMessage chat.db attributedBody NSAttributedString format in Node.js

283 lines 9.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.IMessageDatabase = void 0; const sqlite3_1 = require("sqlite3"); const path = __importStar(require("path")); const os = __importStar(require("os")); const attributed_string_parser_1 = require("./parsers/attributed-string-parser"); class IMessageDatabase { constructor(dbPath, parserOptions) { const finalPath = dbPath || IMessageDatabase.DEFAULT_DB_PATH; this.db = new sqlite3_1.Database(finalPath, (err) => { if (err) { throw new Error(`Failed to open database: ${err.message}`); } }); this.parser = new attributed_string_parser_1.AttributedStringParser(parserOptions); } /** * Get all chats */ async getChats() { return new Promise((resolve, reject) => { const query = ` SELECT ROWID, guid, chat_identifier, display_name FROM chat ORDER BY ROWID DESC `; this.db.all(query, (err, rows) => { if (err) reject(err); else resolve(rows); }); }); } /** * Get messages from a specific chat */ async getMessagesFromChat(chatId, limit = 100, offset = 0) { return new Promise((resolve, reject) => { const query = ` SELECT m.ROWID, m.guid, m.text, m.attributedBody, m.date, m.is_from_me, h.id as handle_id, m.cache_has_attachments FROM message m LEFT JOIN handle h ON m.handle_id = h.ROWID LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id WHERE cmj.chat_id = ? ORDER BY m.date DESC LIMIT ? OFFSET ? `; this.db.all(query, [chatId, limit, offset], (err, rows) => { if (err) reject(err); else resolve(rows); }); }); } /** * Parse a message row to extract text content * Always includes a link to the message (empty string if no GUID) */ parseMessage(message) { let result; // If plain text exists, return it if (message.text) { result = { text: message.text, link: message.guid ? `messages://open?guid=${message.guid}` : '', }; } // If attributedBody exists, parse it else if (message.attributedBody) { result = this.parser.parse(message.attributedBody); // Parser doesn't include link, so add it result.link = message.guid ? `messages://open?guid=${message.guid}` : ''; } // No content else { result = { text: '', link: message.guid ? `messages://open?guid=${message.guid}` : '', }; } return result; } /** * Search messages by content */ async searchMessages(searchTerm, limit = 100) { return new Promise((resolve, reject) => { const query = ` SELECT m.ROWID, m.guid, m.text, m.attributedBody, m.date, m.is_from_me, h.id as handle_id, m.cache_has_attachments, c.display_name as chat_display_name FROM message m LEFT JOIN handle h ON m.handle_id = h.ROWID LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id LEFT JOIN chat c ON cmj.chat_id = c.ROWID WHERE m.text LIKE ? OR m.attributedBody IS NOT NULL ORDER BY m.date DESC LIMIT ? `; this.db.all(query, [`%${searchTerm}%`, limit], (err, rows) => { if (err) { reject(err); return; } // Filter results that actually contain the search term const filtered = rows.filter(row => { if (row.text && row.text.includes(searchTerm)) { return true; } if (row.attributedBody) { const parsed = this.parseMessage(row); return parsed.text.toLowerCase().includes(searchTerm.toLowerCase()); } return false; }); resolve(filtered); }); }); } /** * Get messages with attributedBody in a date range */ async getAttributedMessagesInRange(startDate, endDate, chatName) { return new Promise((resolve, reject) => { // Convert dates to Core Data format (seconds since 2001-01-01) const startTimestamp = Math.floor(startDate.getTime() / 1000) - 978307200; const endTimestamp = Math.floor(endDate.getTime() / 1000) - 978307200; let query = ` SELECT m.ROWID, m.guid, m.text, m.attributedBody, m.date, m.is_from_me, h.id as handle_id, m.cache_has_attachments FROM message m LEFT JOIN handle h ON m.handle_id = h.ROWID LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id LEFT JOIN chat c ON cmj.chat_id = c.ROWID WHERE m.attributedBody IS NOT NULL AND m.date >= ? AND m.date <= ? `; const params = [startTimestamp * 1000000000, endTimestamp * 1000000000]; if (chatName) { query += ' AND c.display_name = ?'; params.push(chatName); } query += ' ORDER BY m.date'; this.db.all(query, params, (err, rows) => { if (err) reject(err); else resolve(rows); }); }); } /** * Get messages with full chat information for building complete links */ async getMessagesWithChatInfo(chatId, limit = 100, offset = 0) { return new Promise((resolve, reject) => { const query = ` SELECT m.ROWID, m.guid, m.text, m.attributedBody, m.date, m.is_from_me, h.id as handle_id, m.cache_has_attachments, c.guid as chat_guid, c.chat_identifier, c.display_name as chat_display_name FROM message m LEFT JOIN handle h ON m.handle_id = h.ROWID LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id LEFT JOIN chat c ON cmj.chat_id = c.ROWID WHERE cmj.chat_id = ? ORDER BY m.date DESC LIMIT ? OFFSET ? `; this.db.all(query, [chatId, limit, offset], (err, rows) => { if (err) reject(err); else resolve(rows); }); }); } /** * Parse a message with chat context to include appropriate links * Provides better link fallbacks using chat information when message GUID is missing */ parseMessageWithChat(message) { const result = this.parseMessage(message); // If no message-specific link, try to build a better one with chat context if (!result.link && message.chat_identifier) { // Check if it's a group chat if (message.chat_identifier.startsWith('chat') && /^chat\d+$/.test(message.chat_identifier)) { result.link = message.chat_guid ? `messages://open?guid=${message.chat_guid}` : ''; } else { result.link = `sms://${encodeURIComponent(message.chat_identifier)}`; } } return result; } /** * Close the database connection */ close() { return new Promise((resolve, reject) => { this.db.close(err => { if (err) reject(err); else resolve(); }); }); } } exports.IMessageDatabase = IMessageDatabase; IMessageDatabase.DEFAULT_DB_PATH = path.join(os.homedir(), 'Library', 'Messages', 'chat.db'); //# sourceMappingURL=imessage-database.js.map