UNPKG

imessage-ts

Version:

TypeScript library for interacting with iMessage on macOS - send messages, monitor chats, and automate responses

778 lines 35.3 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.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