Initial Commit

This commit is contained in:
Autumn Naber
2021-01-07 21:44:24 -08:00
commit 289f44e2e1
6 changed files with 612 additions and 0 deletions

19
README.md Normal file
View File

@@ -0,0 +1,19 @@
# ChatterBox
Chatterbox allows users to set up a web interface into their iMessages.
## Setup
- Make sure your `/Users/<USERNAME>/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`

302
chatterbox.php Normal file
View File

@@ -0,0 +1,302 @@
<?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;
}
}
}
?>

48
index.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
define('__ROOT__', dirname(__FILE__));
require_once(__ROOT__.'/user_actions.php');
?>
<html>
<head>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" type="text/css" rel="stylesheet">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<!-- jQuery library -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<!-- Latest compiled JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<!-- Custom Code -->
<script src="scripts.js"></script>
<link rel="stylesheet" href="stylesheet.css">
</head>
<body>
<div class="container">
<div class="messaging">
<div class="inbox_msg">
<div class="inbox_people">
<div class="inbox_chat" id="inbox_chat">
<?php $chatterbox->display_chat_list(); ?>
</div>
</div>
<div class="mesgs">
<div class="msg_history" id="msg_history">
<?php $chatterbox->display_chat_history(); ?>
</div>
<div class="type_msg">
<span class="input_msg_write">
<input type="text" class="write_msg" placeholder="Type a message" />
</span>
<!-- Commented out until I figure out how to send messages
<span class="input_btn_submit">
<button class="msg_send_btn" type="button">
<i class="fa fa-paper-plane-o" aria-hidden="true"></i>
</button>
</span>
-->
</div>
</div>
</div>
</div>
</div>
</body>
</html>

71
scripts.js Normal file
View File

@@ -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);

140
stylesheet.css Normal file
View File

@@ -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%;
}

32
user_actions.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
if (!defined('__ROOT__')) {
define('__ROOT__', dirname(__FILE__));
}
require_once(__ROOT__.'/chatterbox.php');
if(session_status()!=PHP_SESSION_ACTIVE) {
session_start();
}
if (!isset($_SESSION['chatterbox'])) {
// Database initialization
error_log("creating new session" . PHP_EOL);
$_SESSION['chatterbox'] = new ChatterBox();
}
$chatterbox = $_SESSION['chatterbox'];
if(isset($_POST['action'])) {
if($_POST['action'] == 'refresh_chat_list') {
$chatterbox->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;
}
?>