imessage-parser
Version:
Parse iMessage chat.db attributedBody NSAttributedString format in Node.js
283 lines • 9.7 kB
JavaScript
;
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