imessage-ts
Version:
TypeScript library for interacting with iMessage on macOS - send messages, monitor chats, and automate responses
778 lines • 35.3 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.MessageRepository = void 0;
const types_1 = require("../types");
const process = __importStar(require("process"));
/**
* Repository for accessing messages from the iMessage database
*/
class MessageRepository {
constructor(database) {
this.database = database;
}
/**
* Helper function to clean up extracted text by removing binary data
*/
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 messages from a conversation
*/
async getMessages(options) {
const { conversationId, limit = 50, offset = 0, before, after, fromHandle, onlyFromMe, onlyWithAttachments, } = options;
let query = `
SELECT
m.*, -- Get all fields for debugging
m.ROWID as id,
m.guid,
m.text,
m.attributedBody,
m.subject,
m.cache_has_attachments,
m.cache_roomnames,
m.associated_message_type,
m.expressive_send_style_id,
m.ck_sync_state,
m.message_summary_info,
h.id as handle,
h.ROWID as handleId,
m.service,
datetime(m.date / 1000000000 + 978307200, 'unixepoch', 'localtime') as date,
datetime(m.date_read / 1000000000 + 978307200, 'unixepoch', 'localtime') as dateRead,
datetime(m.date_delivered / 1000000000 + 978307200, 'unixepoch', 'localtime') as dateDelivered,
m.is_from_me as isFromMe,
m.is_audio_message as isAudioMessage,
m.is_played as isPlayed,
cmj.chat_id as chatId
FROM message m
LEFT JOIN handle h ON m.handle_id = h.ROWID
INNER JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
WHERE 1=1
`;
const params = {};
if (conversationId) {
query += ` AND cmj.chat_id = $conversationId`;
params.conversationId = conversationId;
}
if (before) {
query += ` AND m.date < $before`;
params.before = this.convertDateToAppleTimestamp(before);
}
if (after) {
query += ` AND m.date > $after`;
params.after = this.convertDateToAppleTimestamp(after);
}
if (fromHandle) {
query += ` AND h.id = $fromHandle`;
params.fromHandle = fromHandle;
}
if (onlyFromMe) {
query += ` AND m.is_from_me = 1`;
}
if (onlyWithAttachments) {
query += ` AND EXISTS (SELECT 1 FROM attachment WHERE attachment.ROWID IN (SELECT attachment_id FROM message_attachment_join WHERE message_id = m.ROWID))`;
}
query += ` ORDER BY m.date DESC LIMIT $limit OFFSET $offset`;
params.limit = limit;
params.offset = offset;
const messagesWithRawData = await this.database.query(query, params);
// Process the messages to create proper entities and keep raw data for debugging
const messages = messagesWithRawData.map(msg => {
// Create a copy of the raw data for debugging
const processedMsg = { ...msg, _rawData: { ...msg } };
// Default values for missing fields
processedMsg.isDeleted = false;
// If text field is null, try to get text from attributedBody if available
if (processedMsg.text === null && processedMsg.attributedBody) {
try {
// Check if it's a Buffer or string
const attributedBodyStr = Buffer.isBuffer(processedMsg.attributedBody)
? processedMsg.attributedBody.toString('utf8')
: String(processedMsg.attributedBody);
// From test output, we can see attributedBody contains text after NSString marker
// The format appears to be: streamtyped...NSString+{ACTUAL_MESSAGE_TEXT}
// Extract text after the NSString pattern
const nsStringMatch = attributedBodyStr.match(/NSString[^\+]+\+([^\n\r]+)/);
if (nsStringMatch && nsStringMatch[1]) {
processedMsg.text = this.cleanMessageText(nsStringMatch[1]);
}
else {
// Try just taking any text
const textMatch = attributedBodyStr.match(/\+([^\n\r]+)/);
if (textMatch && textMatch[1]) {
processedMsg.text = this.cleanMessageText(textMatch[1]);
}
}
}
catch (e) {
// Silently fail if extraction fails
console.log('Failed to extract text from attributedBody:', e);
}
}
// Also try to extract text from other potential fields
if (processedMsg.text === null) {
// Search various fields that might contain message content
if (processedMsg.subject) {
processedMsg.text = `Subject: ${processedMsg.subject}`;
}
else if (processedMsg.balloon_bundle_id) {
processedMsg.text = `[${processedMsg.balloon_bundle_id} content]`;
}
else if (processedMsg.associated_message_guid) {
processedMsg.text = `[Message reaction or association]`;
}
else if (processedMsg.message_summary_info) {
try {
// If it's a Buffer, convert to string
const summaryInfoStr = Buffer.isBuffer(processedMsg.message_summary_info)
? processedMsg.message_summary_info.toString('utf8')
: String(processedMsg.message_summary_info);
// Try to extract meaningful parts if possible
const textMatch = summaryInfoStr.match(/([a-zA-Z0-9\s\.,!\?]+)/);
if (textMatch && textMatch[1] && textMatch[1].length > 5) {
processedMsg.text = this.cleanMessageText(textMatch[1]);
}
else {
processedMsg.text = '[Message with attachment or effect]';
}
}
catch (e) {
// Silently fail if parsing fails
}
}
else if (processedMsg.expressive_send_style_id) {
// For messages with effects
processedMsg.text = `[Message with effect: ${processedMsg.expressive_send_style_id}]`;
}
else if (processedMsg.isFromMe) {
// For outgoing messages with null text, add a placeholder
processedMsg.text = '[Outgoing message with no visible content]';
}
}
// If still null, set to empty string
if (processedMsg.text === null) {
processedMsg.text = '';
}
return processedMsg;
});
// Fetch attachments for each message
for (const message of messages) {
message.attachments = await this.getMessageAttachments(message.id);
// Set default type and status if not set
if (message.type === undefined) {
message.type = message.attachments?.length ? types_1.MessageType.ATTACHMENT : types_1.MessageType.TEXT;
}
if (message.status === undefined) {
if (message.dateRead) {
message.status = types_1.MessageStatus.READ;
}
else if (message.dateDelivered) {
message.status = types_1.MessageStatus.DELIVERED;
}
else {
message.status = types_1.MessageStatus.SENT;
}
}
}
return messages;
}
/**
* Get a message by ID
*/
async getMessageById(id) {
const query = `
SELECT
m.*, -- Get all fields for debugging
m.ROWID as id,
m.guid,
m.text,
m.attributedBody,
m.subject,
m.balloon_bundle_id,
m.cache_has_attachments,
m.cache_roomnames,
m.associated_message_type,
m.associated_message_guid,
m.expressive_send_style_id,
m.ck_sync_state,
m.message_summary_info,
h.id as handle,
h.ROWID as handleId,
m.service,
datetime(m.date / 1000000000 + 978307200, 'unixepoch', 'localtime') as date,
datetime(m.date_read / 1000000000 + 978307200, 'unixepoch', 'localtime') as dateRead,
datetime(m.date_delivered / 1000000000 + 978307200, 'unixepoch', 'localtime') as dateDelivered,
m.is_from_me as isFromMe,
m.is_audio_message as isAudioMessage,
m.is_played as isPlayed,
cmj.chat_id as chatId
FROM message m
LEFT JOIN handle h ON m.handle_id = h.ROWID
INNER JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
WHERE m.ROWID = $id
`;
let message = await this.database.queryOne(query, { id });
if (message) {
// Create a copy of the raw data for debugging
const processedMsg = { ...message, _rawData: { ...message } };
// Default values for missing fields
processedMsg.isDeleted = false;
// If text field is null, try to get text from attributedBody if available
if (processedMsg.text === null && processedMsg.attributedBody) {
try {
// Check if it's a Buffer or string
const attributedBodyStr = Buffer.isBuffer(processedMsg.attributedBody)
? processedMsg.attributedBody.toString('utf8')
: String(processedMsg.attributedBody);
// From test output, we can see attributedBody contains text after NSString marker
// The format appears to be: streamtyped...NSString+{ACTUAL_MESSAGE_TEXT}
// Extract text after the NSString pattern
const nsStringMatch = attributedBodyStr.match(/NSString[^\+]+\+([^\n\r]+)/);
if (nsStringMatch && nsStringMatch[1]) {
processedMsg.text = this.cleanMessageText(nsStringMatch[1]);
}
else {
// Try just taking any text
const textMatch = attributedBodyStr.match(/\+([^\n\r]+)/);
if (textMatch && textMatch[1]) {
processedMsg.text = this.cleanMessageText(textMatch[1]);
}
}
}
catch (e) {
// Silently fail if extraction fails
console.log('Failed to extract text from attributedBody:', e);
}
}
// Also try to extract text from other potential fields
if (processedMsg.text === null) {
// Search various fields that might contain message content
if (processedMsg.subject) {
processedMsg.text = `Subject: ${processedMsg.subject}`;
}
else if (processedMsg.balloon_bundle_id) {
processedMsg.text = `[${processedMsg.balloon_bundle_id} content]`;
}
else if (processedMsg.associated_message_guid) {
processedMsg.text = `[Message reaction or association]`;
}
else if (processedMsg.message_summary_info) {
try {
// If it's a Buffer, convert to string
const summaryInfoStr = Buffer.isBuffer(processedMsg.message_summary_info)
? processedMsg.message_summary_info.toString('utf8')
: String(processedMsg.message_summary_info);
// Try to extract meaningful parts if possible
const textMatch = summaryInfoStr.match(/([a-zA-Z0-9\s\.,!\?]+)/);
if (textMatch && textMatch[1] && textMatch[1].length > 5) {
processedMsg.text = this.cleanMessageText(textMatch[1]);
}
else {
processedMsg.text = '[Message with attachment or effect]';
}
}
catch (e) {
// Silently fail if parsing fails
}
}
else if (processedMsg.expressive_send_style_id) {
// For messages with effects
processedMsg.text = `[Message with effect: ${processedMsg.expressive_send_style_id}]`;
}
else if (processedMsg.isFromMe) {
// For outgoing messages with null text, add a placeholder
processedMsg.text = '[Outgoing message with no visible content]';
}
}
// If still null, set to empty string
if (processedMsg.text === null) {
processedMsg.text = '';
}
message = processedMsg;
message.attachments = await this.getMessageAttachments(message.id);
// Set default type and status if not set
if (message.type === undefined) {
message.type = message.attachments?.length ? types_1.MessageType.ATTACHMENT : types_1.MessageType.TEXT;
}
if (message.status === undefined) {
if (message.dateRead) {
message.status = types_1.MessageStatus.READ;
}
else if (message.dateDelivered) {
message.status = types_1.MessageStatus.DELIVERED;
}
else {
message.status = types_1.MessageStatus.SENT;
}
}
}
return message;
}
/**
* Get the latest messages across all conversations
*/
async getLatestMessages(limit = 10) {
const query = `
SELECT
m.*, -- Get all fields for debugging
m.ROWID as id,
m.guid,
m.text,
m.attributedBody,
m.subject,
m.balloon_bundle_id,
m.cache_has_attachments,
m.cache_roomnames,
m.associated_message_type,
m.associated_message_guid,
m.expressive_send_style_id,
m.ck_sync_state,
m.message_summary_info,
h.id as handle,
h.ROWID as handleId,
m.service,
datetime(m.date / 1000000000 + 978307200, 'unixepoch', 'localtime') as date,
datetime(m.date_read / 1000000000 + 978307200, 'unixepoch', 'localtime') as dateRead,
datetime(m.date_delivered / 1000000000 + 978307200, 'unixepoch', 'localtime') as dateDelivered,
m.is_from_me as isFromMe,
m.is_audio_message as isAudioMessage,
m.is_played as isPlayed,
cmj.chat_id as chatId
FROM message m
LEFT JOIN handle h ON m.handle_id = h.ROWID
INNER JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
ORDER BY m.date DESC LIMIT $limit
`;
const messagesWithRawData = await this.database.query(query, { limit });
// Process the messages to create proper entities and keep raw data for debugging
const messages = messagesWithRawData.map(msg => {
// Create a copy of the raw data for debugging
const processedMsg = { ...msg, _rawData: { ...msg } };
// Default values for missing fields
processedMsg.isDeleted = false;
// If text field is null, try to get text from attributedBody if available
if (processedMsg.text === null && processedMsg.attributedBody) {
try {
// Check if it's a Buffer or string
const attributedBodyStr = Buffer.isBuffer(processedMsg.attributedBody)
? processedMsg.attributedBody.toString('utf8')
: String(processedMsg.attributedBody);
// From test output, we can see attributedBody contains text after NSString marker
// The format appears to be: streamtyped...NSString+{ACTUAL_MESSAGE_TEXT}
// Extract text after the NSString pattern
const nsStringMatch = attributedBodyStr.match(/NSString[^\+]+\+([^\n\r]+)/);
if (nsStringMatch && nsStringMatch[1]) {
processedMsg.text = this.cleanMessageText(nsStringMatch[1]);
}
else {
// Try just taking any text
const textMatch = attributedBodyStr.match(/\+([^\n\r]+)/);
if (textMatch && textMatch[1]) {
processedMsg.text = this.cleanMessageText(textMatch[1]);
}
}
}
catch (e) {
// Silently fail if extraction fails
console.log('Failed to extract text from attributedBody:', e);
}
}
// Also try to extract text from other potential fields
if (processedMsg.text === null) {
// Search various fields that might contain message content
if (processedMsg.subject) {
processedMsg.text = `Subject: ${processedMsg.subject}`;
}
else if (processedMsg.balloon_bundle_id) {
processedMsg.text = `[${processedMsg.balloon_bundle_id} content]`;
}
else if (processedMsg.associated_message_guid) {
processedMsg.text = `[Message reaction or association]`;
}
else if (processedMsg.message_summary_info) {
try {
// If it's a Buffer, convert to string
const summaryInfoStr = Buffer.isBuffer(processedMsg.message_summary_info)
? processedMsg.message_summary_info.toString('utf8')
: String(processedMsg.message_summary_info);
// Try to extract meaningful parts if possible
const textMatch = summaryInfoStr.match(/([a-zA-Z0-9\s\.,!\?]+)/);
if (textMatch && textMatch[1] && textMatch[1].length > 5) {
processedMsg.text = this.cleanMessageText(textMatch[1]);
}
else {
processedMsg.text = '[Message with attachment or effect]';
}
}
catch (e) {
// Silently fail if parsing fails
}
}
else if (processedMsg.expressive_send_style_id) {
// For messages with effects
processedMsg.text = `[Message with effect: ${processedMsg.expressive_send_style_id}]`;
}
else if (processedMsg.isFromMe) {
// For outgoing messages with null text, add a placeholder
processedMsg.text = '[Outgoing message with no visible content]';
}
}
// If still null, set to empty string
if (processedMsg.text === null) {
processedMsg.text = '';
}
return processedMsg;
});
// Fetch attachments for each message
for (const message of messages) {
message.attachments = await this.getMessageAttachments(message.id);
// Set default type and status if not set
if (message.type === undefined) {
message.type = message.attachments?.length ? types_1.MessageType.ATTACHMENT : types_1.MessageType.TEXT;
}
if (message.status === undefined) {
if (message.dateRead) {
message.status = types_1.MessageStatus.READ;
}
else if (message.dateDelivered) {
message.status = types_1.MessageStatus.DELIVERED;
}
else {
message.status = types_1.MessageStatus.SENT;
}
}
}
return messages;
}
/**
* Get messages after a specific date
*/
async getMessagesAfterDate(date) {
const timestamp = this.convertDateToAppleTimestamp(date);
const query = `
SELECT
m.*, -- Get all fields for debugging
m.ROWID as id,
m.guid,
m.text,
m.attributedBody,
m.subject,
m.balloon_bundle_id,
m.cache_has_attachments,
m.cache_roomnames,
m.associated_message_type,
m.associated_message_guid,
m.expressive_send_style_id,
m.ck_sync_state,
m.message_summary_info,
h.id as handle,
h.ROWID as handleId,
m.service,
datetime(m.date / 1000000000 + 978307200, 'unixepoch', 'localtime') as date,
datetime(m.date_read / 1000000000 + 978307200, 'unixepoch', 'localtime') as dateRead,
datetime(m.date_delivered / 1000000000 + 978307200, 'unixepoch', 'localtime') as dateDelivered,
m.is_from_me as isFromMe,
m.is_audio_message as isAudioMessage,
m.is_played as isPlayed,
cmj.chat_id as chatId
FROM message m
LEFT JOIN handle h ON m.handle_id = h.ROWID
INNER JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
WHERE m.date > $timestamp
ORDER BY m.date DESC
`;
const messagesWithRawData = await this.database.query(query, { timestamp });
// Process the messages to create proper entities and keep raw data for debugging
const messages = messagesWithRawData.map(msg => {
// Create a copy of the raw data for debugging
const processedMsg = { ...msg, _rawData: { ...msg } };
// Default values for missing fields
processedMsg.isDeleted = false;
// If text field is null, try to get text from attributedBody if available
if (processedMsg.text === null && processedMsg.attributedBody) {
try {
// Check if it's a Buffer or string
const attributedBodyStr = Buffer.isBuffer(processedMsg.attributedBody)
? processedMsg.attributedBody.toString('utf8')
: String(processedMsg.attributedBody);
// From test output, we can see attributedBody contains text after NSString marker
// The format appears to be: streamtyped...NSString+{ACTUAL_MESSAGE_TEXT}
// Extract text after the NSString pattern
const nsStringMatch = attributedBodyStr.match(/NSString[^\+]+\+([^\n\r]+)/);
if (nsStringMatch && nsStringMatch[1]) {
processedMsg.text = this.cleanMessageText(nsStringMatch[1]);
}
else {
// Try just taking any text
const textMatch = attributedBodyStr.match(/\+([^\n\r]+)/);
if (textMatch && textMatch[1]) {
processedMsg.text = this.cleanMessageText(textMatch[1]);
}
}
}
catch (e) {
// Silently fail if extraction fails
console.log('Failed to extract text from attributedBody:', e);
}
}
// Also try to extract text from other potential fields
if (processedMsg.text === null) {
// Search various fields that might contain message content
if (processedMsg.subject) {
processedMsg.text = `Subject: ${processedMsg.subject}`;
}
else if (processedMsg.balloon_bundle_id) {
processedMsg.text = `[${processedMsg.balloon_bundle_id} content]`;
}
else if (processedMsg.associated_message_guid) {
processedMsg.text = `[Message reaction or association]`;
}
else if (processedMsg.message_summary_info) {
try {
// If it's a Buffer, convert to string
const summaryInfoStr = Buffer.isBuffer(processedMsg.message_summary_info)
? processedMsg.message_summary_info.toString('utf8')
: String(processedMsg.message_summary_info);
// Try to extract meaningful parts if possible
const textMatch = summaryInfoStr.match(/([a-zA-Z0-9\s\.,!\?]+)/);
if (textMatch && textMatch[1] && textMatch[1].length > 5) {
processedMsg.text = this.cleanMessageText(textMatch[1]);
}
else {
processedMsg.text = '[Message with attachment or effect]';
}
}
catch (e) {
// Silently fail if parsing fails
}
}
else if (processedMsg.expressive_send_style_id) {
// For messages with effects
processedMsg.text = `[Message with effect: ${processedMsg.expressive_send_style_id}]`;
}
else if (processedMsg.isFromMe) {
// For outgoing messages with null text, add a placeholder
processedMsg.text = '[Outgoing message with no visible content]';
}
}
// If still null, set to empty string
if (processedMsg.text === null) {
processedMsg.text = '';
}
return processedMsg;
});
// Fetch attachments for each message
for (const message of messages) {
message.attachments = await this.getMessageAttachments(message.id);
// Set default type and status if not set
if (message.type === undefined) {
message.type = message.attachments?.length ? types_1.MessageType.ATTACHMENT : types_1.MessageType.TEXT;
}
if (message.status === undefined) {
if (message.dateRead) {
message.status = types_1.MessageStatus.READ;
}
else if (message.dateDelivered) {
message.status = types_1.MessageStatus.DELIVERED;
}
else {
message.status = types_1.MessageStatus.SENT;
}
}
}
return messages;
}
/**
* Get attachments for a message
*/
async getMessageAttachments(messageId) {
const query = `
SELECT
a.ROWID as id,
maj.message_id as messageId,
a.filename as fileName,
a.transfer_name as transferName,
a.total_bytes as fileSize,
a.mime_type as mimeType,
datetime(a.created_date / 1000000000 + 978307200, 'unixepoch', 'localtime') as created
FROM attachment a
INNER JOIN message_attachment_join maj ON a.ROWID = maj.attachment_id
WHERE maj.message_id = $messageId
`;
const attachments = await this.database.query(query, { messageId });
// Determine attachment type based on MIME type, handling null values
for (const attachment of attachments) {
attachment.filePath = this.getAttachmentPath(attachment);
// Default to FILE type if mimeType is null or undefined
if (!attachment.mimeType) {
attachment.type = types_1.AttachmentType.FILE;
continue;
}
if (attachment.mimeType.startsWith('image/')) {
attachment.type = types_1.AttachmentType.IMAGE;
}
else if (attachment.mimeType.startsWith('video/')) {
attachment.type = types_1.AttachmentType.VIDEO;
}
else if (attachment.mimeType.startsWith('audio/')) {
attachment.type = types_1.AttachmentType.AUDIO;
}
else {
attachment.type = types_1.AttachmentType.FILE;
}
}
return attachments;
}
/**
* Get attachments across multiple messages
*/
async getAttachments(options) {
const { messageId, conversationId, types, limit = 50, offset = 0 } = options;
let query = `
SELECT
a.ROWID as id,
maj.message_id as messageId,
a.filename as fileName,
a.transfer_name as transferName,
a.total_bytes as fileSize,
a.mime_type as mimeType,
datetime(a.created_date / 1000000000 + 978307200, 'unixepoch', 'localtime') as created
FROM attachment a
INNER JOIN message_attachment_join maj ON a.ROWID = maj.attachment_id
INNER JOIN message m ON maj.message_id = m.ROWID
INNER JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
WHERE 1=1
`;
const params = { limit, offset };
if (messageId) {
query += ` AND maj.message_id = $messageId`;
params.messageId = messageId;
}
if (conversationId) {
query += ` AND cmj.chat_id = $conversationId`;
params.conversationId = conversationId;
}
if (types && types.length > 0) {
const typeConditions = types.map((type, index) => {
const paramName = `type${index}`;
let condition = '';
if (type === 'image') {
condition = `a.mime_type LIKE 'image/%'`;
}
else if (type === 'video') {
condition = `a.mime_type LIKE 'video/%'`;
}
else if (type === 'audio') {
condition = `a.mime_type LIKE 'audio/%'`;
}
else {
condition = `a.mime_type NOT LIKE 'image/%' AND a.mime_type NOT LIKE 'video/%' AND a.mime_type NOT LIKE 'audio/%'`;
}
return condition;
});
query += ` AND (${typeConditions.join(' OR ')})`;
}
query += ` ORDER BY a.created_date DESC LIMIT $limit OFFSET $offset`;
const attachments = await this.database.query(query, params);
// Determine attachment type based on MIME type, handling null values
for (const attachment of attachments) {
attachment.filePath = this.getAttachmentPath(attachment);
// Default to FILE type if mimeType is null or undefined
if (!attachment.mimeType) {
attachment.type = types_1.AttachmentType.FILE;
continue;
}
if (attachment.mimeType.startsWith('image/')) {
attachment.type = types_1.AttachmentType.IMAGE;
}
else if (attachment.mimeType.startsWith('video/')) {
attachment.type = types_1.AttachmentType.VIDEO;
}
else if (attachment.mimeType.startsWith('audio/')) {
attachment.type = types_1.AttachmentType.AUDIO;
}
else {
attachment.type = types_1.AttachmentType.FILE;
}
}
return attachments;
}
/**
* Get the full path to an attachment file
*/
getAttachmentPath(attachment) {
// The actual path in the filesystem would require parsing the filename field
// which contains a relative path within the attachments directory
if (!attachment.fileName) {
return '';
}
// Check if fileName already contains a full path or starts with ~/
if (attachment.fileName.startsWith('~/')) {
// Expand the tilde to home directory
return attachment.fileName.replace(/^~/, process.env.HOME || '');
}
else if (attachment.fileName.startsWith('/')) {
// Already an absolute path
return attachment.fileName;
}
else {
// Relative path - prepend the attachments directory
return `${this.database.getAttachmentsPath()}/${attachment.fileName}`;
}
}
/**
* Convert a JavaScript Date to Apple's timestamp format (nanoseconds since 2001-01-01)
*/
convertDateToAppleTimestamp(date) {
// Apple's timestamp is nanoseconds since 2001-01-01
const appleEpoch = new Date('2001-01-01').getTime();
return (date.getTime() - appleEpoch) * 1000000;
}
}
exports.MessageRepository = MessageRepository;
//# sourceMappingURL=message-repository.js.map