303 lines
9.7 KiB
PHP
Executable File
303 lines
9.7 KiB
PHP
Executable File
<?php
|
|
class ChatterBox {
|
|
const HANDLE_ID = 'Handle';
|
|
const DATE_ID = 'Date';
|
|
const TEXT_ID = 'Text';
|
|
const CHAT_ID = 'Chat';
|
|
const ISSENT_ID = 'IsSent';
|
|
const FIRST_ID = "FirstName";
|
|
const LAST_ID = "LastName";
|
|
|
|
const CHAT_DB = __ROOT__.'/chat.db';
|
|
const ADDR_DB = __ROOT__.'/AddressBook-v22.abcddb';
|
|
|
|
/* TODO: IMPORTANT: Replace these with your user's messages and address book files */
|
|
const CHAT_DB_SRC = '/Users/<USERNAME>/Library/Messages/chat.db';
|
|
const ADDR_DB_SRC = '/Users/<USERNAME>/Application\ Support/AddressBook/AddressBook-v22.abcddb';
|
|
|
|
public $chat_ids;
|
|
public $selected_chat;
|
|
public $filewatch;
|
|
|
|
function __construct() {
|
|
date_default_timezone_set('America/Los_Angeles');
|
|
$this->chat_ids = ChatterBox::get_chat_list();
|
|
$this->selected_chat = $this->chat_ids[0];
|
|
// Watch the write-ahead log, since that's what stores recent changes
|
|
$this->filewatch = new FileWatch(ChatterBox::CHAT_DB_SRC . '-wal');
|
|
}
|
|
|
|
function __destruct() {
|
|
}
|
|
|
|
public function check_for_changes() {
|
|
$filechanged = $this->filewatch->has_changed();
|
|
if ($filechanged) {
|
|
$this->filewatch->copy_database(ChatterBox::CHAT_DB_SRC, __ROOT__ . '/');
|
|
}
|
|
echo $filechanged;
|
|
}
|
|
|
|
public function get_chat_messages($chat_id) {
|
|
$chat_db = new SQLite3(ChatterBox::CHAT_DB, SQLITE3_OPEN_READONLY);
|
|
if (!$chat_db) {
|
|
error_log($chat_db->lastErrorMsg());
|
|
}
|
|
|
|
$query =
|
|
"SELECT m.date AS " . ChatterBox::DATE_ID . ", " .
|
|
"m.text AS " . ChatterBox::TEXT_ID . ", " .
|
|
"m.is_from_me AS " . ChatterBox::ISSENT_ID . ", " .
|
|
"h.id AS " . ChatterBox::HANDLE_ID . " " .
|
|
"FROM message AS m " .
|
|
"LEFT OUTER JOIN chat_message_join AS cm " .
|
|
"ON cm.message_id = m.ROWID " .
|
|
"LEFT OUTER JOIN handle as h " .
|
|
"ON m.handle_id = h.ROWID " .
|
|
"WHERE cm.chat_id = $chat_id " .
|
|
"ORDER BY m.date ASC;";
|
|
|
|
$ret = $chat_db->query($query);
|
|
if(!$ret) {
|
|
error_log( "Unable to query database. " . $chat_db->lastErrorMsg() );
|
|
} else {
|
|
while($row = $ret->fetchArray(SQLITE3_ASSOC)) {
|
|
$chat_messages[] = $row;
|
|
}
|
|
}
|
|
|
|
$chat_db->close();
|
|
return $chat_messages;
|
|
}
|
|
|
|
public static function get_chat_list() {
|
|
$chat_ids = array();
|
|
$chat_db = new SQLite3(ChatterBox::CHAT_DB, SQLITE3_OPEN_READONLY);
|
|
if (!$chat_db) {
|
|
error_log($chat_db->lastErrorMsg());
|
|
return $chat_ids;
|
|
}
|
|
|
|
$query =
|
|
"SELECT DISTINCT(cm.chat_id) AS " . ChatterBox::CHAT_ID . " " .
|
|
"FROM chat_message_join AS cm " .
|
|
"ORDER BY cm.message_date;";
|
|
|
|
$ret = $chat_db->query($query);
|
|
if(!$ret) {
|
|
error_log( "Unable to query database. " . $chat_db->lastErrorMsg() );
|
|
} else {
|
|
while($row = $ret->fetchArray(SQLITE3_ASSOC)) {
|
|
$chat_ids[] = $row[ChatterBox::CHAT_ID];
|
|
}
|
|
}
|
|
|
|
$chat_db->close();
|
|
return $chat_ids;
|
|
}
|
|
|
|
public function get_chat_handles($chat_id) {
|
|
$handles = array();
|
|
$chat_db = new SQLite3(ChatterBox::CHAT_DB, SQLITE3_OPEN_READONLY);
|
|
if (!$chat_db) {
|
|
error_log($chat_db->lastErrorMsg());
|
|
return $handles;
|
|
}
|
|
|
|
$query =
|
|
"SELECT h.id AS " . ChatterBox::HANDLE_ID . " " .
|
|
"FROM chat_handle_join AS ch " .
|
|
"INNER JOIN handle as h " .
|
|
" on ch.handle_id = h.ROWID " .
|
|
"WHERE ch.chat_id = $chat_id;";
|
|
|
|
$ret = $chat_db->query($query);
|
|
if(!$ret) {
|
|
error_log( "Unable to query database. " . $chat_db::lastErrorMsg() );
|
|
} else {
|
|
while($row = $ret->fetchArray(SQLITE3_ASSOC)) {
|
|
$handles[] = $row[ChatterBox::HANDLE_ID];
|
|
}
|
|
}
|
|
|
|
$chat_db->close();
|
|
return $handles;
|
|
}
|
|
|
|
public function get_chat_previews() {
|
|
$chat_previews = array();
|
|
$chat_db = new SQLite3(ChatterBox::CHAT_DB, SQLITE3_OPEN_READONLY);
|
|
if (!$chat_db) {
|
|
error_log($chat_db->lastErrorMsg());
|
|
return $chat_previews;
|
|
}
|
|
|
|
$query =
|
|
"SELECT MAX(cm.message_date) AS " . ChatterBox::DATE_ID . ", " .
|
|
"cm.chat_id AS " . ChatterBox::CHAT_ID . ", " .
|
|
"m.text AS " . ChatterBox::TEXT_ID . " " .
|
|
"FROM chat_message_join AS cm " .
|
|
"INNER JOIN message AS m " .
|
|
"ON cm.message_id = m.ROWID " .
|
|
"GROUP BY cm.chat_id " .
|
|
"ORDER BY cm.message_date DESC;";
|
|
|
|
$this->chat_ids = array();
|
|
$ret = $chat_db->query($query);
|
|
if(!$ret) {
|
|
error_log( "Unable to query database. " . $chat_db->lastErrorMsg() );
|
|
} else {
|
|
while($row = $ret->fetchArray(SQLITE3_ASSOC)) {
|
|
$this->chat_ids[] = $row[ChatterBox::CHAT_ID];
|
|
$chat_previews[] = $row;
|
|
}
|
|
}
|
|
|
|
$chat_db->close();
|
|
return $chat_previews;
|
|
}
|
|
|
|
public function convert_epoch_to_date($epoch) {
|
|
return date("F d", substr($epoch, 0, -3));
|
|
}
|
|
|
|
public function convert_epoch_to_datetime($epoch) {
|
|
return date("H:i \| F d", substr($epoch, 0, -3));
|
|
}
|
|
|
|
public function get_names_from_chat($chat_id) {
|
|
$handles = $this->get_chat_handles($chat_id);
|
|
$names = $this->get_names_from_handles($handles);
|
|
|
|
return $names;
|
|
}
|
|
|
|
public function get_names_from_handles($handles) {
|
|
$addr_db = new SQLite3(ChatterBox::ADDR_DB, SQLITE3_OPEN_READONLY);
|
|
if (!$addr_db) {
|
|
error_log($addr_db->lastErrorMsg());
|
|
}
|
|
|
|
$names = array();
|
|
foreach($handles as $handle) {
|
|
// Remove + from beginning of phone numbers
|
|
$handle = preg_replace("/^\+/", "", $handle);
|
|
$query =
|
|
"SELECT r.ZFIRSTNAME AS " . ChatterBox::FIRST_ID . ", " .
|
|
"r.ZLASTNAME AS " . ChatterBox::LAST_ID . " " .
|
|
"FROM ZABCDRECORD AS r " .
|
|
"INNER JOIN ZABCDCONTACTINDEX as i " .
|
|
"ON i.ZCONTACT = r.ROWID " .
|
|
"WHERE i.ZSTRINGFORINDEXING LIKE \"%" . $handle . "%\";";
|
|
|
|
$ret = $addr_db->querySingle($query, true);
|
|
if($ret == false) {
|
|
$names[] = $handle;
|
|
} else {
|
|
$names[] = $ret[ChatterBox::FIRST_ID] . " " . $ret[ChatterBox::LAST_ID];
|
|
}
|
|
}
|
|
|
|
$addr_db->close();
|
|
return join(", ", $names);
|
|
}
|
|
|
|
public function display_chat_list() {
|
|
$chat_list = $this->get_chat_previews();
|
|
$chat_ids = array();
|
|
|
|
for($index = 0; $index < count($chat_list); $index++) {
|
|
$chat_id = $chat_list[$index][ChatterBox::CHAT_ID];
|
|
|
|
echo '<div id=' . $chat_id . ' class="chat_list';
|
|
//if($index == $this->selected_index) {
|
|
if($chat_id == $this->selected_chat) {
|
|
echo" active_chat";
|
|
}
|
|
echo '"><h5>';
|
|
echo $this->get_names_from_chat($chat_id);
|
|
//echo $chat_db->get_chat_handles($chat_list[$index][ChatterBox::CHAT_ID]);
|
|
echo '<span class="chat_date">';
|
|
echo $this->convert_epoch_to_date($chat_list[$index][ChatterBox::DATE_ID]);
|
|
echo '</span></h5><p>';
|
|
echo $chat_list[$index][ChatterBox::TEXT_ID];
|
|
echo '</p></div>';
|
|
$chat_ids[] = $chat_id;
|
|
}
|
|
$this->chat_ids = $chat_ids;
|
|
}
|
|
|
|
public function send_message($message, $recipient) {
|
|
//TODO
|
|
}
|
|
|
|
public function display_chat_history() {
|
|
$messages = $this->get_chat_messages($this->selected_chat);
|
|
|
|
for($index = 0; $index < count($messages); $index++) {
|
|
if($messages[$index][ChatterBox::ISSENT_ID]) {
|
|
echo '<div class="sent_message"><p>';
|
|
} else {
|
|
echo '<div class="received_message"><p>';
|
|
}
|
|
echo $messages[$index][ChatterBox::TEXT_ID];
|
|
echo '</p><span class="time_date">';
|
|
echo $this->convert_epoch_to_datetime(
|
|
$messages[$index][ChatterBox::DATE_ID]);
|
|
echo '</span></div>';
|
|
}
|
|
}
|
|
}
|
|
|
|
class FileWatch {
|
|
|
|
protected $watch_file = null;
|
|
protected $last_accessed = 0;
|
|
|
|
public function __construct($filename) {
|
|
$this->watch_file = $filename;
|
|
return true;
|
|
}
|
|
|
|
public function has_changed() {
|
|
$file_changed = false;
|
|
$ret = null;
|
|
$output = null;
|
|
|
|
// Mac-variant
|
|
exec('stat -f %m ' . $this->watch_file, $output, $ret);
|
|
// POSIX-variant
|
|
// exec('stat -t -c %Y ' . $this->watch_file, $output, $ret);
|
|
if($ret) {
|
|
error_log("Unable to access file: " . $this->watch_file);
|
|
} else {
|
|
if(!$this->last_accessed || (int)$output[0] > $this->last_accessed) {
|
|
$this->last_accessed = (int)$output[0];
|
|
$file_changed = true;
|
|
}
|
|
}
|
|
|
|
return $file_changed;
|
|
}
|
|
|
|
function copy_database($source, $destination) {
|
|
$db_file = basename($source);
|
|
$src = dirname($source) . '/';
|
|
$dst = $destination;
|
|
|
|
// Copy database file
|
|
if (!copy($src . $db_file, $dst . $db_file)) {
|
|
error_log('Could not copy database');
|
|
return;
|
|
}
|
|
|
|
// Copy write-ahead log
|
|
if (!copy($src . $db_file . '-wal', $dst . $db_file . '-wal')) {
|
|
error_log('Could not copy write-ahead log');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
?>
|