Skip to content

Instantly share code, notes, and snippets.

@jeffbargmann
Last active February 25, 2021 23:53
Show Gist options
  • Save jeffbargmann/a9ec1559c9d07b18807e0be2afae4989 to your computer and use it in GitHub Desktop.
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.
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)];
}
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);
};
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;
}
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