Last active
February 25, 2021 23:53
-
-
Save jeffbargmann/a9ec1559c9d07b18807e0be2afae4989 to your computer and use it in GitHub Desktop.
iMessage Database Parser for macOS. Generates notification packets for new messages in unmuted chats.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var nodeContacts = require('node-mac-contacts') | |
module.exports = ContactsDB; | |
function ContactsDB(opts) { | |
opts = opts || {}; | |
var status = nodeContacts.getAuthStatus(); | |
this.loadContacts(); | |
this.indexContacts(); | |
} | |
ContactsDB.prototype.loadContacts = function(data) { | |
if(!this.allContacts) { | |
this.allContacts = nodeContacts.getAllContacts(); | |
} | |
} | |
ContactsDB.prototype.indexContacts = function(data) { | |
var index = {}; | |
var self = this; | |
this.allContacts.forEach(contact=>{ | |
contact.name = self.normalizeName(contact); | |
contact.phoneNumbers && contact.phoneNumbers.forEach(phoneNumber=>{ | |
index[self.normalizeData(phoneNumber)] = contact; | |
}) | |
contact.emailAddresses && contact.emailAddresses.forEach(emailAddress=>{ | |
index[self.normalizeData(emailAddress)] = contact; | |
}) | |
}); | |
this.index = index; | |
} | |
ContactsDB.prototype.normalizeName = function(contact) { | |
if(contact.nickname && contact.nickname.length) { | |
return contact.nickname; | |
} | |
if(contact.firstName && contact.firstName.length && | |
contact.lastName && contact.lastName.length) { | |
return contact.firstName + ' ' + contact.lastName; | |
} | |
if(contact.firstName && contact.firstName.length) { | |
return contact.firstName; | |
} | |
if(contact.lastName && contact.lastName.length) { | |
return contact.lastName; | |
} | |
return 'Unknown'; | |
} | |
ContactsDB.prototype.normalizeData = function(data) { | |
if(!data) return ''; | |
data = data.toLowerCase(); | |
data = data.replace('(', ''); | |
data = data.replace('(', ''); | |
data = data.replace(')', ''); | |
data = data.replace(')', ''); | |
data = data.replace('-', ''); | |
data = data.replace('-', ''); | |
data = data.replace('-', ''); | |
data = data.replace('.', ''); | |
data = data.replace('.', ''); | |
data = data.replace('.', ''); | |
data = data.replace('.', ''); | |
data = data.replace(' ', ''); | |
data = data.replace(' ', ''); | |
if(data.length == 12 && !data.includes('@') && data[0] == '+' && data[1] == '1') { | |
data = data.substring(2); | |
} | |
data = data.replace('+', ''); | |
data = data.replace('@', ''); | |
return data; | |
} | |
ContactsDB.prototype.search = function(data) { | |
return this.index[this.normalizeData(data)]; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const sqlite3 = require('sqlite3').verbose(); | |
const path = require('path'); | |
const Q = require('q'); | |
const bplist = require('bplist-parser'); | |
var HOME = getUserHome(); | |
var identities = []; | |
function getUserHome() { | |
var envVar = (process.platform == 'win32') ? 'USERPROFILE' : 'HOME'; | |
return process.env[envVar]; | |
} | |
module.exports = iMessageDB; | |
function iMessageDB(opts) { | |
opts = opts || {}; | |
this.path = opts.path || iMessageDB.DB_PATH; | |
this.db = this.connect(); | |
} | |
iMessageDB.OSX_EPOCH = 978307200; | |
iMessageDB.DB_PATH = path.join(HOME, '/Library/Messages/chat.db'); | |
iMessageDB.prototype.connect = function () { | |
var deferred = Q.defer(); | |
var path = this.path; | |
var db = new sqlite3.Database( | |
path, | |
sqlite3.OPEN_READONLY, | |
function (err, res) { | |
if (err) return deferred.reject(err); | |
return deferred.resolve(db); | |
}); | |
return deferred.promise; | |
}; | |
iMessageDB.prototype.getDb = function (cb) { | |
var args = arguments; | |
// nodeify? | |
this.db | |
.then(function (db) { | |
cb(null, db); | |
}, function (err) { | |
cb(err); | |
}); | |
}; | |
iMessageDB.prototype.getQuery = async function (query) { | |
var self = this; | |
return new Promise(function (resolve, reject) { | |
self.db.done(function (db) { | |
db.all(query, (err, result) => { | |
if (err) return reject(err); | |
else resolve(result); | |
}); | |
}); | |
}); | |
}; | |
iMessageDB.prototype.getMessagesSinceID = async function (lastId, maxCount, includeSender) { | |
if(!lastId) { | |
return null; | |
} | |
var where = ` WHERE \`message\`.ROWID > ${lastId}`; | |
var join = ""; | |
if (includeSender) join = " JOIN `handle` ON `handle`.ROWID = `message`.handle_id"; | |
var query = `SELECT * FROM \`message\`${join}${where} ORDER BY \`message\`.ROWID DESC` | |
return this.getQuery(query); | |
}; | |
iMessageDB.prototype.getChatIdFromMessageId = async function (messageId) { | |
if(!messageId) { | |
return null; | |
} | |
var result = await this.getQuery(`SELECT chat_id FROM chat_message_join WHERE message_id = ${messageId}`); | |
return ((result && result.length) ? result[0].chat_id : null); | |
}; | |
iMessageDB.prototype.getChatFromChatId = async function (chat_id) { | |
if(!chat_id) { | |
return null; | |
} | |
var result = await this.getQuery(`SELECT * FROM chat WHERE ROWID = ${chat_id}`); | |
var row = ((result && result.length) ? result[0] : null); | |
if(!row) { | |
return null; | |
} | |
var properties = {}; | |
if(row.properties) { | |
var pList =await bplist.parseBuffer(row.properties); | |
var pListProperties = pList && pList[0]; | |
properties = pListProperties || {} | |
if(properties.ignoreAlertsFlag) { | |
properties.ignoreAlertsFlag = properties.ignoreAlertsFlag; | |
} | |
} | |
row.properties = properties; | |
return row; | |
}; | |
iMessageDB.prototype.getHandleFromHandleId = async function (handle_id) { | |
if(!handle_id) { | |
return null; | |
} | |
var result = await this.getQuery(`SELECT * FROM handle WHERE ROWID = ${handle_id}`); | |
return ((result && result.length) ? result[0] : null); | |
}; | |
iMessageDB.prototype.getFirstAttachmentIdFromMessageId = async function (messageId) { | |
if(!messageId) { | |
return null; | |
} | |
var result = await this.getQuery(`SELECT attachment_id FROM message_attachment_join WHERE message_id = ${messageId}`); | |
return ((result && result.length) ? result[0].attachment_id : null); | |
}; | |
iMessageDB.prototype.getAttachmentFromAttachmentId = async function (attachment_id) { | |
if(!attachment_id) { | |
return null; | |
} | |
var result = await this.getQuery(`SELECT * FROM attachment WHERE ROWID = ${attachment_id}`); | |
var row = ((result && result.length) ? result[0] : null); | |
if(!row) { | |
return null; | |
} | |
return row; | |
}; | |
iMessageDB.prototype.getMaxMessageRowID = async function () { | |
var result = await this.getQuery("SELECT MAX(message.ROWID) AS maxid FROM message"); | |
return ((result && result.length) ? result[0].maxid : null); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const iMessageDB = require('./iMessageDB'); | |
const ContactsDB = require('./ContactsDB'); | |
const bplist = require('bplist-parser'); | |
let imDB = new iMessageDB(); | |
module.exports = iMessageDBParser; | |
function iMessageDBParser(opts) { | |
opts = opts || {}; | |
} | |
iMessageDBParser.prototype.getMaxMessageRowID = async function(lastId) { | |
return imDB.getMaxMessageRowID(); | |
} | |
iMessageDBParser.prototype.getNewReceivedMessagesSince = async function(lastId) { | |
if(!lastId) { | |
return; | |
} | |
var contactsSearch = new ContactsDB(); | |
var messages = await imDB.getMessagesSinceID(lastId, 100, false); | |
var messagesFiltered = []; | |
for(var messageIndex = 0 ; messageIndex < messages.length ; messageIndex++) { | |
var message = messages[messageIndex]; | |
// Check from me | |
if(message.is_from_me || !message.handle_id) { | |
continue; | |
} | |
if(message.is_empty) { | |
continue; | |
} | |
// Check for old | |
const coreDataEpoch = 978307200000; | |
var dateJS = message.date/1000/1000 + coreDataEpoch; | |
var timeSinceSent = (new Date().getTime() - dateJS); | |
if(message.date <= 0 || (timeSinceSent > 1000*60*60*24)) { | |
continue; | |
} | |
// Check for DND | |
var chatId = await imDB.getChatIdFromMessageId(message.ROWID); | |
var chat = await imDB.getChatFromChatId(chatId); | |
if(chat && chat.properties && chat.properties.ignoreAlertsFlag) { | |
continue; | |
} | |
// Check for read | |
if(message.date_read || message.is_read) { | |
continue; | |
} | |
// Get sender | |
var handle = await imDB.getHandleFromHandleId(message.handle_id) | |
if(!handle) { | |
continue; | |
} | |
var contact = contactsSearch.search(handle.id); | |
var contactName = contact && contact.name || handle.id; | |
if(!contact || !contact.name) { | |
contact = contact; | |
} | |
// Lookup attachment | |
var attachmentId = await imDB.getFirstAttachmentIdFromMessageId(message.ROWID) | |
var attachment = attachmentId && await imDB.getAttachmentFromAttachmentId(attachmentId); | |
if(attachment && !attachment.mime_type) { | |
attachment = null; //ignore Apple internal for now, i.e. ballons | |
//balloon_bundle_id:"com.apple.messages.URLBalloonProvider" | |
} | |
var attachmentTxt = ''; | |
var mime_type = attachment && attachment.mime_type; | |
if(mime_type && attachment.mime_type.startsWith('video/')) { | |
attachmentTxt = 'Video' | |
} else if(mime_type && attachment.mime_type.startsWith('image/gif')) { | |
attachmentTxt = 'Gif' | |
} else if(mime_type && attachment.mime_type.startsWith('image/')) { | |
attachmentTxt = 'Image' | |
} else if(mime_type && attachment.mime_type.startsWith('audio/')) { | |
attachmentTxt = 'Audio' | |
} else if(mime_type && attachment.mime_type.startsWith('application/pdf')) { | |
attachmentTxt = 'PDF file attachment' | |
} else if(mime_type && attachment.mime_type.startsWith('text/x-vlocation')) { | |
attachmentTxt = 'Location attachment' | |
} else if(mime_type && attachment.mime_type.startsWith('text/vcard')) { | |
attachmentTxt = 'Contact card attachment' | |
} else if(attachment) { | |
attachmentTxt = 'Attachment' | |
} | |
// Compose message | |
var notificationTxt = message.text || ''; | |
notificationTxt = notificationTxt.replace("", ''); //empty char | |
if(attachmentTxt) { | |
notificationTxt = `[${attachmentTxt}]${(notificationTxt.length?' ':'')}${notificationTxt}` | |
} | |
if(!notificationTxt.length) { | |
continue; | |
} | |
messagesFiltered.push({ | |
date: message.date, | |
from: contactName, | |
fromId: handle.id, | |
fromHandleId: handle && handle.ROWID, | |
text: notificationTxt, | |
textOriginal: message.text || '', | |
messageId: message.ROWID, | |
messageChatId: chat && chat.ROWID, | |
attachment: attachment && attachment.filename, | |
attachmentId: attachment && attachment.ROWID, | |
attachmentMimeType: attachment && attachment.mime_type, | |
attachmentTypeText: attachment && attachmentTxt | |
}); | |
} | |
return messagesFiltered; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let iMessageDBParser = require('./iMessageDBParser'); | |
let imDB = new iMessageDBParser(); | |
module.exports = iMessagePump; | |
function iMessagePump(cb, opts) { | |
opts = opts || {}; | |
this.cb = cb; | |
} | |
iMessagePump.prototype.POLL_FREQUENCY = 1000; | |
iMessagePump.prototype.stop = async function() { | |
if(!this.runLoopTimer) { | |
return; | |
} | |
this.runLoopTimer = null; | |
this.maxIndex = undefined; | |
} | |
iMessagePump.prototype.run = async function() { | |
if(this.runLoopTimer) { | |
return; | |
} | |
var self = this; | |
this.runLoopTimer = setInterval(()=>{ | |
self.checkForMessages(); | |
}, this.POLL_FREQUENCY); | |
} | |
iMessagePump.prototype.checkForMessages = async function() { | |
var maxIndexNew = await imDB.getMaxMessageRowID(); | |
const backtrackCount = 10; | |
this.maxIndex = this.maxIndex || (maxIndexNew - backtrackCount); | |
if(maxIndexNew < this.maxIndex) { | |
maxIndexNew = this.maxIndex; | |
} | |
if(this.maxIndex == maxIndexNew) { | |
return; | |
} | |
var maxIndexLast = this.maxIndex; | |
this.maxIndex = maxIndexNew; | |
var messages = await imDB.getNewReceivedMessagesSince(maxIndexLast); | |
if(messages && messages.length) { | |
this.cb && this.cb(messages, maxIndexNew); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment