imessage-ts
Version:
TypeScript library for interacting with iMessage on macOS - send messages, monitor chats, and automate responses
244 lines • 8.45 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.exploreDatabase = exploreDatabase;
exports.getDatabaseSchema = getDatabaseSchema;
exports.getTableColumns = getTableColumns;
exports.getRecentMessages = getRecentMessages;
exports.searchMessages = searchMessages;
exports.getMessageByGuid = getMessageByGuid;
exports.getAttributedBodyExamples = getAttributedBodyExamples;
const sqlite3_1 = __importDefault(require("sqlite3"));
const path_1 = __importDefault(require("path"));
const os_1 = __importDefault(require("os"));
/**
* Utility script to directly query the iMessage database and explore its structure
*/
const DEFAULT_DB_PATH = path_1.default.join(os_1.default.homedir(), 'Library/Messages/chat.db');
/**
* Connect directly to the iMessage database and run queries
*/
async function exploreDatabase(query) {
return new Promise((resolve, reject) => {
// Open a direct connection to the database
const db = new sqlite3_1.default.Database(DEFAULT_DB_PATH, sqlite3_1.default.OPEN_READONLY, (err) => {
if (err) {
reject(`Error opening database: ${err.message}`);
return;
}
console.log('Connected to iMessage database');
// Execute the query
db.all(query, [], (err, rows) => {
// Close the database connection
db.close();
if (err) {
reject(`Error executing query: ${err.message}`);
return;
}
resolve(rows);
});
});
});
}
/**
* Helper function to clean up extracted text by removing binary data
*/
function cleanMessageText(text) {
// Find the index of the first non-printable or binary character
const firstBinaryIndex = text.search(/[^\x20-\x7E]/);
if (firstBinaryIndex !== -1) {
text = text.substring(0, firstBinaryIndex);
}
// The attributed string format appears to include a single character prefix
// (N or 6 in observed examples) that is not part of the message.
// Remove this first character if the text is long enough
if (text.length > 1) {
return text.substring(1);
}
return text;
}
/**
* Get database schema information
*/
async function getDatabaseSchema() {
return exploreDatabase(`
SELECT
name as table_name,
sql as create_statement
FROM
sqlite_master
WHERE
type='table'
ORDER BY
name
`);
}
/**
* Get table columns
*/
async function getTableColumns(tableName) {
return exploreDatabase(`PRAGMA table_info(${tableName})`);
}
/**
* Get the most recent messages with all columns
*/
async function getRecentMessages(limit = 10) {
const messages = await exploreDatabase(`
SELECT
m.*,
datetime(m.date / 1000000000 + 978307200, 'unixepoch', 'localtime') as formatted_date,
h.id as handle_address
FROM
message m
LEFT JOIN
handle h ON m.handle_id = h.ROWID
ORDER BY
m.date DESC
LIMIT ${limit}
`);
// Process messages to extract text from attributedBody when text is null
return messages.map(msg => {
// If text is null but attributedBody exists, try to extract text
if (msg.text === null && msg.attributedBody) {
try {
// Check if it's a Buffer or string
const attributedBodyStr = Buffer.isBuffer(msg.attributedBody)
? msg.attributedBody.toString('utf8')
: String(msg.attributedBody);
// Extract text after the NSString pattern
const nsStringMatch = attributedBodyStr.match(/NSString[^\+]+\+([^\n\r]+)/);
if (nsStringMatch && nsStringMatch[1]) {
msg.text = cleanMessageText(nsStringMatch[1]);
}
else {
// Try just taking any text after a plus sign
const textMatch = attributedBodyStr.match(/\+([^\n\r]+)/);
if (textMatch && textMatch[1]) {
msg.text = cleanMessageText(textMatch[1]);
}
}
}
catch (e) {
// Silently fail
}
}
return msg;
});
}
/**
* Search for a specific string in message columns
*/
async function searchMessages(searchString) {
// Escape single quotes for SQL
const escapedSearch = searchString.replace(/'/g, "''");
const messages = await exploreDatabase(`
SELECT
m.*,
datetime(m.date / 1000000000 + 978307200, 'unixepoch', 'localtime') as formatted_date,
h.id as handle_address
FROM
message m
LEFT JOIN
handle h ON m.handle_id = h.ROWID
WHERE
m.text LIKE '%${escapedSearch}%'
OR m.attributedBody LIKE '%${escapedSearch}%'
OR m.associated_message_guid LIKE '%${escapedSearch}%'
OR m.message_summary_info LIKE '%${escapedSearch}%'
ORDER BY
m.date DESC
LIMIT 20
`);
// Process messages to extract text from attributedBody when text is null
return messages.map(msg => {
// If text is null but attributedBody exists, try to extract text
if (msg.text === null && msg.attributedBody) {
try {
// Check if it's a Buffer or string
const attributedBodyStr = Buffer.isBuffer(msg.attributedBody)
? msg.attributedBody.toString('utf8')
: String(msg.attributedBody);
// Extract text after the NSString pattern
const nsStringMatch = attributedBodyStr.match(/NSString[^\+]+\+([^\n\r]+)/);
if (nsStringMatch && nsStringMatch[1]) {
msg.text = cleanMessageText(nsStringMatch[1]);
}
else {
// Try just taking any text after a plus sign
const textMatch = attributedBodyStr.match(/\+([^\n\r]+)/);
if (textMatch && textMatch[1]) {
msg.text = cleanMessageText(textMatch[1]);
}
}
}
catch (e) {
// Silently fail
}
}
return msg;
});
}
/**
* Get data for a specific message GUID
*/
async function getMessageByGuid(guid) {
const messages = await exploreDatabase(`
SELECT
m.*,
datetime(m.date / 1000000000 + 978307200, 'unixepoch', 'localtime') as formatted_date,
h.id as handle_address
FROM
message m
LEFT JOIN
handle h ON m.handle_id = h.ROWID
WHERE
m.guid = '${guid}'
`);
// Process messages to extract text from attributedBody when text is null
return messages.map(msg => {
// If text is null but attributedBody exists, try to extract text
if (msg.text === null && msg.attributedBody) {
try {
// Check if it's a Buffer or string
const attributedBodyStr = Buffer.isBuffer(msg.attributedBody)
? msg.attributedBody.toString('utf8')
: String(msg.attributedBody);
// Extract text after the NSString pattern
const nsStringMatch = attributedBodyStr.match(/NSString[^\+]+\+([^\n\r]+)/);
if (nsStringMatch && nsStringMatch[1]) {
msg.text = cleanMessageText(nsStringMatch[1]);
}
else {
// Try just taking any text after a plus sign
const textMatch = attributedBodyStr.match(/\+([^\n\r]+)/);
if (textMatch && textMatch[1]) {
msg.text = cleanMessageText(textMatch[1]);
}
}
}
catch (e) {
// Silently fail
}
}
return msg;
});
}
// Functions to explore message data structures
async function getAttributedBodyExamples() {
return exploreDatabase(`
SELECT
ROWID,
attributedBody,
text
FROM
message
WHERE
attributedBody IS NOT NULL
ORDER BY
date DESC
LIMIT 5
`);
}
//# sourceMappingURL=database-explorer.js.map