$ git clone https://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?>