$ git clone http://thingshare.ion.nu/thingshare.git
commit 5df4cf603a6f64e1c582df72c6a45a31a039b9ca
Author: Alicia <...>
Date:   Sun Mar 15 18:30:01 2020 +0100

    Implemented messaging.

diff --git a/db.php b/db.php
index 1e031d4..cd82d63 100644
--- a/db.php
+++ b/db.php
@@ -111,7 +111,7 @@ function db_create_tables()
     recipient text,
     sender text,
     sent datetime,
-    chain integer,
+    chain varchar(500),
     subject text,
     message text,
     msgread boolean,
diff --git a/head.php b/head.php
index 2225807..21f7f8c 100644
--- a/head.php
+++ b/head.php
@@ -19,6 +19,7 @@
 */
 include_once('config.php');
 $menu=''; // Additional menu items depending on privileges/just being logged in
+$unreadmsgs='';
 if(isset($_COOKIE['PHPSESSID'])) // TODO: See if there's a better way to check if there is a session to resume
 {
   session_start();
@@ -35,6 +36,9 @@ if(isset($_COOKIE['PHPSESSID'])) // TODO: See if there's a better way to check i
     }
     $privileges=$res[0];
     if($privileges>0){$menu.='<a href="'.BASEURL.'/admin">Administration</a>';}
+    // Check for unread messages
+    $res=mysqli_query($db, 'select id from messages where user='.(int)$_SESSION['id'].' and !msgread limit 1');
+    if(mysqli_num_rows($res)>0){$unreadmsgs=' class="highlight"';}
   }
 }
 $loginlink=BASEURL.'/login?returnto='.urlencode($_SERVER['REQUEST_URI']);
@@ -58,6 +62,7 @@ $search=htmlentities(isset($_GET['q'])?$_GET['q']:'');
     <div id="user"><?php if(isset($_SESSION['name'])){ ?>
       <!-- Link to user settings, messages -->
       <a href="<?=BASEURL?>/user/<...>">My profile</a>
+      <a href="<?=BASEURL?>/messages"<?=$unreadmsgs?>>Messages</a>
       <a href="<?=$logoutlink?>">Log out</a>
     <?php }else{ ?>
       <a href="<?=$loginlink?>">Log in</a>
diff --git a/index.php b/index.php
index 203e47d..b31bd78 100644
--- a/index.php
+++ b/index.php
@@ -68,6 +68,7 @@ switch($path[1])
       case 'search': include('rpc_search.php'); exit();
       case 'rpckey': print(json_encode(Array('public'=>file_get_contents('rpckey.pem')), true)); exit();
       case 'report': include('rpc_report.php'); exit();
+      case 'messages': include('rpc_messages.php'); exit();
     }
     header('HTTP/1.1 404 Not found');
     die('{"httpresponse":404,"error":"Not found"}');
diff --git a/messages.php b/messages.php
new file mode 100644
index 0000000..5e13479
--- /dev/null
+++ b/messages.php
@@ -0,0 +1,158 @@
+<...>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+include_once('config.php');
+if(isset($_COOKIE['PHPSESSID'])){session_start();}
+if(!isset($_SESSION['id'])){header('Location: '.BASEURL.'/login?returnto='.urlencode($_SERVER['REQUEST_URI']));}
+include_once('db.php');
+include_once('nonce.php');
+include_once('rpc.php');
+function getdisplayname($username)
+{
+  static $cache=Array();
+  if(isset($cache[$username])){return $cache[$username];}
+  $user=explode('@', $username);
+  if(count($user)!=2){return $username;} // Invalid username, fall back on username
+  $obj=rpc_get($user[1], 'user/'.$user[0]);
+  if(isset($obj['error'])){return $username;} // RPC error, fall back on username
+  $cache[$username]=$obj['displayname'];
+  return $obj['displayname'];
+}
+$error='';
+$info='';
+// Resolve chain ID
+if(isset($path[2]) && $path[2]!='new' && $path[2]!='')
+{
+  $chain=mysqli_real_escape_string($db, $path[2]);
+  $res=mysqli_query($db, 'select sender, recipient, subject from messages where user='.(int)$_SESSION['id'].' and chain="'.$chain.'" and latest');
+  if(!($res=mysqli_fetch_row($res)))
+  {
+    $error='Message chain not found';
+  }else{
+    $to=$res[($res[0]==$_SESSION['name'].'@'.DOMAIN)?1:0];
+    $subject=$res[2];
+  }
+}else{
+  $chain='';
+  $to=$_POST['to'];
+  $subject='';
+}
+// Send message
+if($error=='' && isset($_POST['msg']) && isset($_POST['subject']) && ($path[2]!='new' || isset($_POST['to'])) && checknonce())
+{
+  $touser=explode('@', $to);
+  if(count($touser)!=2){$error='Invalid recipient';}else{
+  // Store in DB
+  $subject=$_POST['subject'];
+  $to_esc=mysqli_real_escape_string($db, $to);
+  $from=mysqli_real_escape_string($db, $_SESSION['name'].'@'.DOMAIN);
+  $timestamp=mysqli_real_escape_string($db, date('Y-m-d H:i:s'));
+  $subject_esc=mysqli_real_escape_string($db, $subject);
+  $msg=mysqli_real_escape_string($db, $_POST['msg']);
+  $q='insert into messages(user, recipient, sender, sent, subject, message, msgread, latest'.(($chain=='')?'':', chain').') ';
+  $q.='values('.(int)$_SESSION['id'].', "'.$to_esc.'", "'.$from.'", "'.$timestamp.'", "'.$subject_esc.'", "'.$msg.'", true, true'.(($chain=='')?'':', "'.$chain.'"').')';
+  if(!mysqli_query($db, $q)){$error='Database error, message not sent';}else{
+  $id=(int)mysqli_insert_id($db);
+  if($chain=='') // Set chain ID for new chain
+  {
+    $path[2]=$id.'_'.DOMAIN;
+    $chain=mysqli_real_escape_string($db, $path[2]);
+    mysqli_query($db, 'update messages set chain="'.$chain.'" where id='.(int)$id);
+  }
+  // Send it to recipient's node
+  $msg=Array('subject'=>$subject,
+             'from'=>$_SESSION['name'],
+             'message'=>$_POST['msg'],
+             'chain'=>$path[2]);
+  $data=rpc_post($touser[1], 'messages/'.$touser[0], $msg);
+  if(isset($data['error']))
+  {
+    $error=$data['error'];
+    // Delete the failed message from database
+    mysqli_query($db, 'delete from messages where user='.(int)$_SESSION['id'].' and id='.$id);
+  }else{
+    $info=_('Message sent');
+    // Update 'latest' on messages which are now old
+    mysqli_query($db, 'update messages set latest=false where chain="'.$chain.'" and user='.(int)$_SESSION['id'].' and id!='.$id);
+  }
+  }} // Error checks
+}
+$messages='';
+$header='';
+// One view for overview, one view for thread/new
+if(!isset($path[2]) || $path[2]=='') // Overview
+{
+  $header='<tr><th>'._('To/From').'</th><th>'._('Subject').'</th><th>'._('Date').'</th></tr>';
+  $res=mysqli_query($db, 'select id, recipient, sender, sent, subject, message, msgread, chain from messages where user='.(int)$_SESSION['id'].' and latest order by sent desc');
+  while($row=mysqli_fetch_assoc($res))
+  {
+    $user=(($row['recipient']==$_SESSION['name'].'@'.DOMAIN)?$row['sender']:$row['recipient']);
+    $user='<a href="'.BASEURL.'/user/'.urlencode($user).'" title="'.htmlentities($user).'">'.htmlentities(getdisplayname($user)).'</a>';
+    $subjectline=htmlentities($row['subject']);
+    $chain=htmlentities($row['chain']);
+    $aclass=($row['msgread']?'':' class="highlight"'); // Highlight link if unread
+    $messages.='<tr>';
+    $messages.='  <td>'.$user.'</td>';
+    $messages.='  <td><a href="'.BASEURL.'/messages/'.$chain.'"'.$aclass.'>'.$subjectline.'</a></td>';
+    $messages.='  <td>'.htmlentities($row['sent']).'</td>';
+    $messages.='</tr>';
+  }
+}
+elseif($error=='' && $path[2]!='new') // Thread view
+{
+  include_once('parsedown/Parsedown.php');
+  $md=new Parsedown();
+  $res=mysqli_query($db, 'select id, recipient, sender, sent, subject, message, msgread from messages where user='.(int)$_SESSION['id'].' and chain="'.$chain.'" order by sent asc');
+  while($row=mysqli_fetch_assoc($res))
+  {
+    $sender=htmlentities($row['sender']);
+    $displayname=getdisplayname($row['sender']);
+    $msg=$md->text($row['message']);
+    $time=htmlentities($row['sent']);
+// TODO: CSS for this
+    $messages.='<div class="message'.($row['msgread']?'':' message_unread').'"><div class="message_sender"><a href="'.BASEURL.'/user/'.$sender.'" title="'.$sender.'">'.$displayname.'</a> '.$time.'</div>'.$msg.'</div>';
+  }
+  mysqli_query($db, 'update messages set msgread=true where user='.(int)$_SESSION['id'].' and chain="'.$chain.'" order by sent asc');
+// TODO: Option to block user
+}
+if($path[2]=='new')
+{
+  $to='<label>'._('To:').' <...></label><br />';
+}else{
+  $to=_('To:').' '.$to.'<br />';
+}
+if($error!=''){$info='<span class="error">'.$error.'</span>';}
+include_once('head.php');
+?>
+<h1><?=(($subject=='')?_('Messages'):$subject)?></h1>
+<?=$info?>
+<table>
+  <?=$header?>
+  <?=$messages?>
+</table>
+<?php if(isset($path[2]) && $path[2]!=''){ ?>
+<script src="<?=BASEURL?>/mdjs/mdjs.js"></script>
+<?=(($path[2]=='new')?'':'<h2>'._('Reply').'</h2>')?>
+<form method="post" action="<?=BASEURL?>/messages/<?=$path[2]?>"><?=nonce().$to?>
+  <?=_('Subject:')?> <input type="text" name="subject" value="<?=$subject?>" /><br />
+  <textarea rows="12" style="width:100%;" name="msg" onchange="document.getElementById('mdpreview').innerHTML='Markdown preview:<br />'+Mdjs.md2html(this.value);" onkeyup="this.onchange();"></textarea>
+  <div id="mdpreview"></div>
+  <button><?=_('Send')?></button>
+</form>
+<?php } ?>
diff --git a/rpc_messages.php b/rpc_messages.php
new file mode 100644
index 0000000..5f00957
--- /dev/null
+++ b/rpc_messages.php
@@ -0,0 +1,56 @@
+<...>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+include_once('rpc.php');
+include_once('db.php');
+$obj=rpc_verifypost($peer);
+// Find recipient user ID
+$to=mysqli_real_escape_string($db, $path[3]);
+$res=mysqli_query($db, 'select id from users where name="'.$to.'"');
+$res=mysqli_fetch_row($res);
+if(!$res){header('HTTP/1.1 404 Not found'); die('{"error":"User not found"}');}
+// Prepare values for DB
+$user=(int)$res[0];
+$recipient=mysqli_real_escape_string($db, $path[3].'@'.DOMAIN);
+$sender=mysqli_real_escape_string($db, $obj['from'].'@'.$peer);
+$subject=mysqli_real_escape_string($db, $obj['subject']);
+$msg=mysqli_real_escape_string($db, $obj['message']);
+$chain=mysqli_real_escape_string($db, $obj['chain']);
+$timestamp=mysqli_real_escape_string($db, date('Y-m-d H:i:s'));
+// Check that $user owns the chain and $sender is one of its participants (or that the chain doesn't exist yet)
+$res=mysqli_query($db, 'select sender, recipient from messages where user='.$user.', chain="'.$chain.'" limit 1');
+if($res=mysqli_fetch_row($res))
+{
+  if(!in_array($obj['from'].'@'.$peer, $res))
+  {
+    header('HTTP/1.1 400 Bad request'); die('{"error":"Invalid message chain"}');
+  }
+}
+// Store message
+$q='insert into messages(user, recipient, sender, sent, subject, message, msgread, latest, chain) ';
+$q.='values('.$user.', "'.$recipient.'", "'.$sender.'", "'.$timestamp.'", "'.$subject.'", "'.$msg.'", false, true, "'.$chain.'")';
+if(mysqli_query($db, $q))
+{
+  // Update 'latest' on messages which are now old
+  mysqli_query($db, 'update messages set latest=false where chain="'.$chain.'" and user='.$user.' and id!='.(int)mysqli_insert_id($db));
+  print('{"status":"OK"}');
+}else{
+  print('{"error":"Database error"}');
+}
+?>
diff --git a/style.css b/style.css
index 0fa5518..45b62ce 100644
--- a/style.css
+++ b/style.css
@@ -152,3 +152,9 @@ table {
 tr:nth-child(even) {
   background-color:#e8e8e8;
 }
+div.message_unread {
+  background-color:#d0ffd0;
+}
+a.highlight {
+  font-weight:bold;
+}
diff --git a/user.php b/user.php
index 84d2d27..722334a 100644
--- a/user.php
+++ b/user.php
@@ -57,5 +57,6 @@ foreach($userobj['things'] as $thing)
 // TODO: Profile picture?
 ?>
 <h1><?=$displayname?> <small class="subheader"><?=$username?></small></h1>
+<a href="<?=BASEURL?>/messages/new?to=<?=$username?>"><?=_('Send message')?></a>
 <?=$profile?><br />
 <?=$things?>