commit 289f44e2e1b4842ef233d1de35cc065fa4a2a07e Author: Autumn Naber Date: Thu Jan 7 21:44:24 2021 -0800 Initial Commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..408c437 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# ChatterBox + +Chatterbox allows users to set up a web interface into their iMessages. + +## Setup +- Make sure your `/Users//Library/Messages/{chat.db,chat.db-wal}` files are all-readable, and the folders above it are all read/execute-able. +- Enable Apache and PHP +- Set the `CHAT_DB_SRC` and `ADDR_DB_SRC` variables in `chatterbox.php` +- Move this all into `/Library/WebServer/Documents` (or test it out with the PHP standalone server) + +## Limitations +This is honestly a pretty insecure implementation of a web server, so please use at your own risk +- There's no authentication +- There's no SSL/encryption +- It requires all execute/read permissions on your chat and address book databases + +Additionally, there's no functionality yet to send messages or view attachments. I'm working on that part. AppleScript is just so atrocious to work with. + +This works well for hosting an OSX VM on a local network, so I can use a Windows or Linux box and still have a real screen to read messages on. I'm hoping to set it up to send messages and view attachments to further limit the number of times I have to take my phone out of my pocket or type using my thumbs. `DO NOT USE THIS ON INTERNET-FACING SYSTEMS` diff --git a/chatterbox.php b/chatterbox.php new file mode 100644 index 0000000..544a7fd --- /dev/null +++ b/chatterbox.php @@ -0,0 +1,302 @@ +/Library/Messages/chat.db'; + const ADDR_DB_SRC = '/Users//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 '
'; + echo $this->get_names_from_chat($chat_id); + //echo $chat_db->get_chat_handles($chat_list[$index][ChatterBox::CHAT_ID]); + echo ''; + echo $this->convert_epoch_to_date($chat_list[$index][ChatterBox::DATE_ID]); + echo '

'; + echo $chat_list[$index][ChatterBox::TEXT_ID]; + echo '

'; + $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 '

'; + } else { + echo '

'; + } + echo $messages[$index][ChatterBox::TEXT_ID]; + echo '

'; + echo $this->convert_epoch_to_datetime( + $messages[$index][ChatterBox::DATE_ID]); + echo '
'; + } + } +} + +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; + } + } +} + +?> diff --git a/index.php b/index.php new file mode 100644 index 0000000..30dc720 --- /dev/null +++ b/index.php @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + +
+
+
+
+
+ display_chat_list(); ?> +
+
+
+
+ display_chat_history(); ?> +
+
+ + + + +
+
+
+
+
+ + diff --git a/scripts.js b/scripts.js new file mode 100644 index 0000000..9eaf024 --- /dev/null +++ b/scripts.js @@ -0,0 +1,71 @@ +var timer; +function message_timer() { + clearInterval(timer); + timer = setInterval(function() { + $.ajax({ + method: "POST", + url: 'user_actions.php', + data:{action: 'check_for_changes'}, + success:function(change_detected) { + if(change_detected) { + refresh_chat_list(); + refresh_messages(); + } + } + }); + + }, 5000); +} + +function refresh_messages() { + $.ajax({ + method: "POST", + url: 'user_actions.php', + data:{action: 'refresh_messages'}, + success:function(html) { + $("#msg_history").html(html); + scroll_history(); + } + }); +} + +function refresh_chat_list() { + $.ajax({ + method: "POST", + url: 'user_actions.php', + data: {action: 'refresh_chat_list'}, + success:function(html) { + $("#inbox_chat").html(html); + set_chat_clicked(); + } + }); +} + +function scroll_history() { + var d = $("#msg_history"); + d.scrollTop(d.prop("scrollHeight")); +} + +function set_chat_clicked() { + $(".chat_list").on('click', function(event) { + event.stopPropagation(); + event.stopImmediatePropagation(); + + message_timer(); + $.ajax({ + method: "POST", + url: 'user_actions.php', + data:{action: 'set_selected_chat', + chat_id: $(this).attr('id')}, + success:function(html) { + refresh_chat_list(); + refresh_messages(); + } + }); + }); +} + +$(window).on('load', scroll_history); +$(window).on('load', message_timer); + +$("#inbox_chat").ready(set_chat_clicked); diff --git a/stylesheet.css b/stylesheet.css new file mode 100644 index 0000000..6226980 --- /dev/null +++ b/stylesheet.css @@ -0,0 +1,140 @@ +body,html { + height: 100%; +} + +.container{max-width:1170px; margin:auto;} +img{ max-width:100%;} +.inbox_people { + background: #f8f8f8 none repeat scroll 0 0; + float: left; + overflow: hidden; + width: 40%; border-right:1px solid #c4c4c4; + height: 100%; +} +.inbox_msg { + border: 1px solid #c4c4c4; + clear: both; + overflow: hidden; + height: 100%; +} + +.recent_heading {float: left; width:40%;} +.recent_heading h4 { + color: #05728f; + font-size: 21px; + margin: auto; +} + +.chat_img { + float: left; + width: 11%; +} + +.inbox_chat { + overflow-y: scroll; + height: 100%; +} + + +.chat_list h5{ font-size:15px; color:#464646; margin:0 0 8px 0;} +.chat_list h5 span{ font-size:13px; float:right;} +.chat_list p{ font-size:14px; color:#989898; margin:auto} +.chat_list { + border-bottom: 1px solid #c4c4c4; + margin: 0; + padding: 18px 16px 10px 15px; + overflow:hidden; + clear:both; + float: left; + width: 100%; +} +.active_chat{ background:#ebebeb;} + +.received_message { + overflow:hidden; + margin:13px 0 13px; + display: inline-block; + vertical-align: top; + width: 64%; + padding: 0px 0px 0px 10px; +} +.received_message p { + background: #ebebeb none repeat scroll 0 0; + border-radius: 3px; + color: #646464; + font-size: 14px; + margin: 0; + padding: 5px 10px 5px 12px; + width: 100%; +} + +.sent_message { + overflow:hidden; + margin:13px 0 13px; + float: right; + width: 64%; + margin-right: 10px; +} +.sent_message p { + background: #05728f none repeat scroll 0 0; + border-radius: 3px; + font-size: 14px; + margin: 0; color:#fff; + padding: 5px 5px 5px 12px; + width:100%; +} + +.time_date { + color: #747474; + display: block; + font-size: 12px; + margin: 8px 0 0; +} +.mesgs { + float: left; + padding: 0 0 0 5px; + width: 60%; +} + +.type_msg { + border-top: 1px solid #c4c4c4; + position: relative; + height: 5%; + min-height: 43; +} +.input_msg_write input { + float: left; + background: rgba(0, 0, 0, 0) none repeat scroll 0 0; + border: medium none; + color: #4c4c4c; + font-size: 15px; + height: 100%; + width: 100%; +} +.input_btn_submit { + float: right; +} + +.msg_send_btn { + background: #05728f none repeat scroll 0 0; + border: medium none; + border-radius: 50%; + color: #fff; + cursor: pointer; + font-size: 17px; + height: 33px; + position: absolute; + padding-right: 9px; + right: 5px; + top: 5px; + width: 33px; +} +.messaging { + padding: 5px 5px 5px 5px; + height: 100%; +} +.msg_history { + overflow-y: scroll; + /*height: 95%;*/ + height: 100%; +} diff --git a/user_actions.php b/user_actions.php new file mode 100644 index 0000000..c56e769 --- /dev/null +++ b/user_actions.php @@ -0,0 +1,32 @@ +display_chat_list(); + } else if ($_POST['action'] == 'set_selected_chat') { + $chatterbox->selected_chat = intval($_POST['chat_id']); + } else if ($_POST['action'] == 'refresh_messages') { + $chatterbox->display_chat_history(); + } else if ($_POST['action'] == 'check_for_changes') { + $chatterbox->check_for_changes(); + } else if ($_POST['action'] == 'send_message') { + // TODO Figure out how to send iMessages via command line + } + $_SESSION['chatterbox'] = $chatterbox; +} +?>