Compare commits

19 Commits

Author SHA1 Message Date
0d3c76086b better client for friends 2026-07-24 10:18:30 +10:00
6598162680 I forgot one thing 2026-07-24 09:05:33 +10:00
999a9fe346 update README 2026-07-24 08:39:28 +10:00
1159bdebde Save messages to database 2026-07-23 14:31:13 +10:00
d61946d2ec unread message count + fix bug 2026-07-22 17:34:07 +10:00
abd51e347c start working on friends system 2026-07-22 17:15:05 +10:00
e99d312937 a few qol fixes 2026-07-21 20:04:33 +10:00
dd7e1f8981 Settings 2026-07-21 19:50:36 +10:00
0965749fc8 make profile pages show posts in right order 2026-07-21 17:34:43 +10:00
3a63c68c2d profile pages 2026-07-21 17:31:43 +10:00
09ec5a33ec now you can unlike :( 2026-07-21 17:08:25 +10:00
5d5982c2dc Everyone gets... ONE vote! 2026-07-21 16:58:44 +10:00
7e9271db72 Logins + sessions working 2026-07-21 16:05:08 +10:00
c6a0e91e7e fix 2026-07-21 13:11:20 +10:00
720a101468 Merge pull request 'Update server/src/main.cpp' (#3) from reallynniroprobably/chookchat:reallynniroprobably-patch-1 into master
Reviewed-on: chookspace/chookchat#3
2026-07-21 13:10:19 +10:00
a7dbe2fca3 continue working on login 2026-07-21 13:09:52 +10:00
ec4a9d69de Update server/src/main.cpp 2026-07-21 13:08:59 +10:00
4ce1d56d69 start work on persistent login 2026-07-21 12:06:46 +10:00
cca6737791 Merge pull request 'design navbar for client' (#2) from SpookyDervish/chookchat:master into master
Reviewed-on: chookspace/chookchat#2
2026-07-21 08:02:27 +10:00
17 changed files with 27213 additions and 41 deletions

View File

@@ -6,6 +6,8 @@ Features:
- Username/password access (passwords secured by bcrypt2)
- Markdown rendering
- User profile pages
- Friends (direct messages system) (WIP)
## Building
@@ -24,3 +26,18 @@ meson compile -C builddir
# run with
./builddir/chookchat
```
## Credits
The server uses the following libraries:
- [cpp-httplib](https://github.com/yhirose/cpp-httplib) for the web server
- [Maddy](https://github.com/progsource/maddy) for the Markdown processor
- [Bcrypt.cpp](https://github.com/hilch/Bcrypt.cpp) for password hashing/salting
- [JSON for Modern C++](https://github.com/nlohmann/json) for JSON processing
- [bin2cpp](https://github.com/end2endzone/bin2cpp) for packing site files into the binary
The following people have contributed to the project:
- [SpookyDervish](https://chookspace.com/SpookyDervish) for client navbar and notification sound
- [reallynniroprobably](https://chookspace.com/reallynniroprobably) for error message

View File

@@ -6,6 +6,15 @@ bin2cpp --file=client/header_top.html --output=server/src/generated
bin2cpp --file=client/header_bottom.html --output=server/src/generated
bin2cpp --file=client/footer.html --output=server/src/generated
bin2cpp --file=client/login.html --output=server/src/generated
bin2cpp --file=client/settings.html --output=server/src/generated
bin2cpp --file=client/friends.html --output=server/src/generated
bin2cpp --file=client/e404.html --output=server/src/generated
bin2cpp --file=client/style.css --output=server/src/generated
bin2cpp --file=client/style_friends.css --output=server/src/generated
bin2cpp --file=client/script.js --output=server/src/generated
bin2cpp --file=client/script_friends.js --output=server/src/generated
bin2cpp --file=client/notify.opus --output=server/src/generated

43
client/friends.html Normal file
View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<title>friends</title>
<link rel="stylesheet" type="text/css" href="/style.css">
<link rel="stylesheet" type="text/css" href="/style_friends.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="/script.js"></script>
<script src="/script_friends.js"></script>
</head>
<body>
<nav>
<div id="brand">
<h2><a href="/">chookchat</a></h2>
</div>
<ul>
<li><a id="loginLink" href="/login">login</a></li>
<li><a href="/friends">friends</a></li>
<li><a href="/settings">settings</a></li>
</ul>
</nav>
<div id="friends">
<div id="friends-list">
<div id="friends-header">
<h1>friends</h1>
</div>
<div id="friends-list-links">
<!-- List of friends inserted here -->
</div>
</div>
<div id="friends-messenger">
<div id="friends-messages">
<!-- List of messages inserted here -->
</div>
<div id="friends-message-box">
<textarea id="friends-textarea"></textarea>
<button onclick="sendMessage()">Send</button>
</div>
</div>
</div>
</body>
</html>

View File

@@ -6,13 +6,14 @@
</div>
<ul>
<li><a href="/">home</a></li>
<li><a href="/login">login</a></li>
<li><a id="loginLink" href="/login">login</a></li>
<li><a href="/friends">friends</a></li>
<li><a href="/settings">settings</a></li>
</ul>
</nav>
<div class="postbox">
<form action="/make_post" method="post">
<div class="postbox" id="postbox">
<form id="postbox-form" action="/make_post" method="post">
<div>
<label for="username">Username:</label>
<input name="username" id="username" />
@@ -35,6 +36,3 @@
</form>
</div>
<div id="posts">
<div id="posts-header">
<h1>posts</h1>
</div>

34
client/login.html Normal file
View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<title>login</title>
<link rel="stylesheet" type="text/css" href="/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="/script.js"></script>
</head>
<body>
<nav>
<div id="brand">
<h2><a href="/">chookchat</a></h2>
</div>
<ul>
<li><a id="loginLink" href="/login">login</a></li>
<li><a href="/friends">friends</a></li>
<li><a href="/settings">settings</a></li>
</ul>
</nav>
<div class="login" id="posts">
<h1>login</h1>
<p>if you haven't registered, enter a username that doesn't exist and hit 'register'</p>
<label for="username">Username:</label>
<input name="username" id="username"></input>
<br>
<label for="password">Password:</label>
<input type="password" name="password" id="password"></input>
<br>
<button onclick="login()">Log In</button>
<button onclick="register()">Register</button>
</div>
</body>
</html>

BIN
client/notify.opus Normal file

Binary file not shown.

View File

@@ -1,10 +1,5 @@
async function like(id) {
const usernameBox = document.getElementById('username');
const passwordBox = document.getElementById('password');
const formData = new FormData();
formData.append('username', usernameBox.value);
formData.append('password', passwordBox.value);
formData.append('post', id);
const result = await fetch('/like', {
@@ -12,7 +7,224 @@ async function like(id) {
body: formData
});
if (result.status === 401) {
window.location.href = "/login";
return;
}
if (result.status < 200 || result.status >= 400) {
alert("your post didnt get the like because the server thought your opinion was invalid");
return;
}
}
async function post() {
const postbox = document.getElementById('newpost');
const formData = new FormData();
formData.append('post', postbox.value);
const result = await fetch('/post', {
method: 'POST',
body: formData
});
if (result.status < 200 || result.status >= 400) {
alert("you have been silenced by the server and your post did not go through");
return;
}
window.location.href = "/";
}
async function register() {
const usernameBox = document.getElementById('username');
const passwordBox = document.getElementById('password');
const formData = new FormData();
formData.append('username', usernameBox.value);
formData.append('password', passwordBox.value);
const result = await fetch('/register', {
method: 'POST',
body: formData
});
if (result.status < 200 || result.status >= 400) {
alert(await result.text());
return;
}
window.location.href = "/";
}
async function login() {
const usernameBox = document.getElementById('username');
const passwordBox = document.getElementById('password');
const formData = new FormData();
formData.append('username', usernameBox.value);
formData.append('password', passwordBox.value);
const result = await fetch('/login', {
method: 'POST',
body: formData
});
if (result.status < 200 || result.status >= 400) {
alert("for some reason the server hates you and didn't let you log into your account");
return;
}
window.location.href = "/";
}
async function checkLoginStatus() {
const result = await fetch('/me');
if (result.headers.get('X-Logged-In') !== 'true') {
return {
username: "",
bio: ""
};
}
const obj = {
username: result.headers.get('X-Username'),
bio: result.headers.get('X-Bio')
};
return obj;
}
document.addEventListener('DOMContentLoaded', async function() {
if (window.fetch) {
const userStatus = await checkLoginStatus();
// update nav link / redirect regardless of whether a postbox exists
const loginLink = document.getElementById("loginLink");
if (userStatus.username === "") {
if (window.location.pathname === "/settings" || window.location.pathname === "/friends") {
window.location.href = "/login";
return;
}
} else if (loginLink) {
loginLink.textContent = "hello, " + userStatus.username + "!";
loginLink.href = "/profile/" + userStatus.username;
}
if (userStatus.username !== "" && window.location.pathname === "/settings") {
// fill bio box with the bio
const textarea = document.getElementById("change-bio");
textarea.value = userStatus.bio;
}
const postbox = document.getElementById("postbox");
if (!postbox) {
return;
}
// postbox-specific setup stays here
if (userStatus.username === "") {
const newElement = document.createElement("p");
newElement.textContent = "Welcome to Chookchat!\nTo get posting, click 'login' to log in or create an account.\nEnjoy your stay!";
postbox.appendChild(newElement);
} else {
const helperText = document.createElement("p");
helperText.textContent = "You're posting as " + userStatus.username;
postbox.appendChild(helperText);
const textArea = document.createElement("textarea");
textArea.id = "newpost";
postbox.appendChild(textArea);
const postButton = document.createElement("button");
postButton.textContent = "Post";
postButton.id = "postbutton";
postButton.onclick = post;
postbox.appendChild(postButton);
}
const form = document.getElementById("postbox-form");
if (!form) {
return;
}
form.style.display = 'none';
}
});
async function settingsChangePassword() {
const oldpassword = document.getElementById("change-password-oldpassword");
const newpassword = document.getElementById("change-password");
const formData = new FormData();
formData.append('oldpassword', oldpassword.value);
formData.append('newpassword', newpassword.value);
const result = await fetch('/settings/changePassword', {
method: "POST",
body: formData
});
if (result.status < 200 || result.status >= 400) {
alert("for whatever reason your password wasn't changed");
} else {
alert("your password was changed!");
}
}
async function settingsChangeUsername() {
const password = document.getElementById("change-username-password");
const newusername = document.getElementById("change-username");
const formData = new FormData();
formData.append('password', password.value);
formData.append('newusername', newusername.value);
const result = await fetch('/settings/changeUsername', {
method: "POST",
body: formData
});
if (result.status < 200 || result.status >= 400) {
alert("for whatever reason your username wasn't changed");
} else {
alert("your username was changed!");
}
}
async function settingsChangeBio() {
const bio = document.getElementById("change-bio");
const formData = new FormData();
formData.append('bio', bio.value);
const result = await fetch('/settings/changeBio', {
method: "POST",
body: formData
});
if (result.status < 200 || result.status >= 400) {
alert("for whatever reason your bio wasn't changed");
} else {
alert("your bio was changed!");
}
}
async function invalidateAllSessions() {
const formData = new FormData();
formData.append('a', 'a');
const result = await fetch('/settings/invalidateAllSessions', {
method: "POST",
body: formData
});
if (result.status < 200 || result.status >= 400) {
alert("for whatever reason your sessions weren't invalidated");
} else {
alert("your sessions were invalidated!");
}
}

289
client/script_friends.js Normal file
View File

@@ -0,0 +1,289 @@
class Message {
sender = 0;
content = "";
constructor(sender, content) {
this.sender = sender;
this.content = content;
}
}
// websocket connection
/** @type {WebSocket} */
let socket;
// array of userId's
/** @type {Array<number>} */
let connectedFriends = [];
// map of userId's to arrays of Message objects
/** @type {Map<number, Array<Message>>} */
let recievedMessages = new Map();
// map of userId's to usernames
/** @type {Map<number, string>} */
let usernames = new Map();
// map of userId's to how many unread messages there are
/** @type {Map<number, number>} */
let unreads = new Map();
// userId of the current user who is being talked to
let currentUser = 0;
// userId of the logged-in user (this client). Fetched from /me on load.
// /me responds with headers X-Logged-In, X-Username, X-Bio, X-User-ID
// (no body), so we read the id back out of the response headers.
let selfId = 0;
// notification sound, loaded after ws initialization
let sound;
async function fetchSelfId() {
try {
const res = await fetch("/me");
if (!res.ok) {
console.error("Failed to fetch /me:", res.status);
return;
}
const loggedIn = res.headers.get("X-Logged-In");
if (loggedIn !== "true") {
console.log("Not logged in");
return;
}
const idHeader = res.headers.get("X-User-ID");
selfId = parseInt(idHeader, 10);
} catch (err) {
console.error("Error fetching /me:", err);
}
}
/**
* @param {number} id
* @returns {string}
*/
async function fetchUsername(id) {
try {
const res = await fetch("/userinfo/" + id);
if (!res.ok) {
console.error("Failed to fetch /userinfo/" + id + " :", res.status, await res.text());
return;
}
const usernameHeader = res.headers.get("X-Username");
usernames.set(id, usernameHeader);
return usernameHeader;
} catch (err) {
console.error("Error fetching /me:", err);
}
}
/**
*
* @param {number} id
* @returns {Array<Message>}
*/
async function getMessageHistory(id) {
try {
const res = await fetch("/friends/chatHistory/" + id);
if (!res.ok) {
console.error("Failed to fetch /friends/chatHistory/" + id + " :", res.status, await res.text());
return [];
}
const json = await res.json();
// the server wraps the array in { "messages": [...] }, not a bare array
return json.messages.map(m => new Message(m.sender, m.content));
} catch (err) {
console.error("Error fetching /me:", err);
return [];
}
}
document.addEventListener('DOMContentLoaded', function() {
fetchSelfId();
if (typeof Notification !== "undefined") {
Notification.requestPermission();
}
// create a websocket
socket = new WebSocket("wss://" + document.location.hostname + ":" + document.location.port + "/friends/ws");
socket.onopen = function() {
console.log("connected to server!");
setInterval(() => { // ping the server every 5 seconds
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 3, userId: 0 }));
}
}, 5000);
};
socket.onmessage = async function(event) {
console.log("recieved data", event.data);
const data = JSON.parse(event.data);
switch (data.type) {
case "ok": {
console.log("Server is ok");
break;
}
case "error": {
console.error("Server sent an error:", data.content);
break;
}
case "message": {
console.log("message from", data.userId, ":", data.content);
// update the messages variable
if (recievedMessages.get(data.userId) === undefined) {
// get previously recieved messages
recievedMessages.set(data.userId, await getMessageHistory(data.userId));
}
recievedMessages.get(data.userId).push(new Message(data.userId, data.content));
if (currentUser === data.userId) {
updateMessages();
}
// send a notification to the user
if (document.hidden || currentUser != data.userId) {
if (Notification !== undefined) {
new Notification(usernames.get(data.userId), {body: data.content});
}
if (audio !== undefined) {
audio.play();
}
const currentUnreads = unreads.get(data.userId);
if (currentUnreads === undefined) {
unreads.set(data.userId, 1);
} else {
unreads.set(data.userId, currentUnreads + 1);
}
updateOnlineUsers();
}
break;
}
case "users": {
console.log("updating user list");
connectedFriends = data.content;
updateOnlineUsers();
break;
}
}
}
socket.onerror = function(error) {
console.log(error);
alert("There was an error with the socket! Check console logs for details. Refresh the page to reconnect");
socket.close();
}
socket.onclose = function(event) {
console.log("disconnected from server");
}
audio = new Audio();
audio.preload = "auto";
audio.src = "/notify.opus";
document.body.appendChild(audio);
});
async function updateOnlineUsers() {
const friendsList = document.getElementById("friends-list-links");
friendsList.innerHTML = "";
// Get everyone's username
for (const user of connectedFriends) {
if (usernames.get(user) === undefined) {
await fetchUsername(user);
}
}
for (const user of connectedFriends) {
const newElement = document.createElement("p");
const userUnreads = unreads.get(user);
if (userUnreads !== undefined) {
newElement.textContent = "[" + userUnreads + "] ";
} else {
newElement.textContent = "";
}
newElement.textContent += usernames.get(user);
newElement.onclick = function() {
openUser(user);
}
friendsList.appendChild(newElement);
}
}
/** @param {Message} message */
function renderMessage(message) {
const messagesList = document.getElementById("friends-messages");
const newElement = document.createElement("p");
const who = message.sender === selfId ? "You" : usernames.get(message.sender);
newElement.textContent = who + ": " + message.content;
messagesList.appendChild(newElement);
messagesList.scrollTop = messagesList.scrollHeight;
}
function updateMessages() {
const messages = recievedMessages.get(currentUser);
if (messages === undefined || messages.length === 0) {
return;
}
const message = messages[messages.length - 1];
renderMessage(message);
}
/** @param {number} user */
async function openUser(user) {
currentUser = user;
const messagesList = document.getElementById("friends-messages");
messagesList.innerHTML = "";
if (recievedMessages.get(user) === undefined) {
recievedMessages.set(user, await getMessageHistory(user));
}
// another openUser() call may have run while we were awaiting, and
// switched the user again - bail out if we're no longer viewing this one
if (currentUser !== user) {
return;
}
const messages = recievedMessages.get(user);
for (const message of messages) {
renderMessage(message);
}
unreads.delete(user);
updateOnlineUsers();
}
function sendMessage() {
if (currentUser === 0) {
alert("Select a friend to message first");
return;
}
const textarea = document.getElementById("friends-textarea");
const content = textarea.value.trim();
if (content === "") {
return;
}
if (socket.readyState !== WebSocket.OPEN) {
alert("Not connected to the server. Refresh the page and try again.");
return;
}
socket.send(JSON.stringify({ type: 0, userId: currentUser, content: content }));
// optimistically show the message locally, since the server only
// relays messages between the two other parties, not back to the sender
if (recievedMessages.get(currentUser) === undefined) {
recievedMessages.set(currentUser, []);
}
const sentMessage = new Message(selfId, content);
recievedMessages.get(currentUser).push(sentMessage);
renderMessage(sentMessage);
textarea.value = "";
}

62
client/settings.html Normal file
View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html>
<head>
<title>settings</title>
<link rel="stylesheet" type="text/css" href="/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="/script.js"></script>
</head>
<body>
<nav>
<div id="brand">
<h2><a href="/">chookchat</a></h2>
</div>
<ul>
<li><a id="loginLink" href="/login">login</a></li>
<li><a href="/friends">friends</a></li>
<li><a href="/settings">settings</a></li>
</ul>
</nav>
<div id="posts">
<div id="posts-header">
<h1>settings</h1>
</div>
<div id="password-settings">
<h3>password</h3>
<p>Enter a new password in the box, and click 'change password' to set a new password</p>
<label for="change-password-oldpassword">Old password:</label>
<input id="change-password-oldpassword" type="password"></input>
<br>
<label for="change-password">New password:</label>
<input id="change-password" type="password"></input>
<br>
<button onclick="settingsChangePassword()">Change Password</button>
</div>
<div id="invalidate-sessions">
<h3>invalidate sessions</h3>
<p>this button will invalidate all your sessions, and you will have to log in again on all your devices.</p>
<button onclick="invalidateAllSessions()">Invalidate All Sessions</button>
</div>
<div id="username-settings">
<h3>username</h3>
<p>Enter a new username in the box, and click 'set username' to set a new username</p>
<p>Note: any links to your profile will break! However, any links to your posts will remain.</p>
<label for="change-username">New username:</label>
<input id="change-username"></input>
<br>
<label for="change-username-password">Password:</label>
<input id="change-username-password" type="password"></input>
<br>
<button onclick="settingsChangeUsername()">Change Username</button>
</div>
<div id="bio-settings">
<h3>bio</h3>
<p>set a new bio here!</p>
<textarea id="change-bio"></textarea>
<br>
<button onclick="settingsChangeBio()">Change Bio</button>
</div>
</div>
</body>
</html>

View File

@@ -62,6 +62,11 @@ img {
gap: 5px;
}
a {
color: lime;
text-decoration: underline;
}
nav {
background: rgb(25, 25, 25);
display: flex;
@@ -98,3 +103,4 @@ nav {
font-size: 16px;
}
}

113
client/style_friends.css Normal file
View File

@@ -0,0 +1,113 @@
#friends {
display: flex;
height: calc(100vh - 60px);
overflow: hidden;
}
#friends-list {
width: 280px;
min-width: 280px;
border-right: 1px solid lime;
display: flex;
flex-direction: column;
overflow: hidden;
}
#friends-header {
padding: 16px;
border-bottom: 1px solid lime;
}
#friends-header h1 {
margin: 0;
font-size: 1.3rem;
text-transform: uppercase;
letter-spacing: 2px;
}
#friends-list-links {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
#friends-list-links p {
margin: 0;
padding: 10px 16px;
cursor: pointer;
border-left: 2px solid transparent;
transition: background-color 0.15s, border-color 0.15s;
}
#friends-list-links p:hover {
background-color: rgba(0, 255, 0, 0.1);
border-left-color: lime;
}
#friends-messenger {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
#friends-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 6px;
}
#friends-messages p {
margin: 0;
padding: 4px 0;
word-wrap: break-word;
}
#friends-messages p:empty {
display: none;
}
#friends-message-box {
position: static;
width: 100%;
display: flex;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid lime;
box-sizing: border-box;
}
#friends-message-box textarea {
flex: 1;
resize: none;
height: 40px;
padding: 8px;
box-sizing: border-box;
}
#friends-message-box button {
padding: 0 20px;
white-space: nowrap;
}
@media (max-width: 600px) {
#friends {
flex-direction: column;
}
#friends-list {
width: 100%;
min-width: unset;
max-height: 200px;
border-right: none;
border-bottom: 1px solid lime;
}
#friends-message-box {
position: static;
width: 100%;
}
}

View File

@@ -8,8 +8,14 @@ sources = [
'src/generated/header_top.cpp',
'src/generated/header_bottom.cpp',
'src/generated/footer.cpp',
'src/generated/login.cpp',
'src/generated/settings.cpp',
'src/generated/friends.cpp',
'src/generated/script_friends.cpp',
'src/generated/style.cpp',
'src/generated/style_friends.cpp',
'src/generated/script.cpp',
'src/generated/notify.cpp',
# bcrypt
'src/bcrypt/bcrypt.cpp',

View File

@@ -1,8 +1,32 @@
#include "db.h"
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <fstream>
#include <ostream>
#include <sqlite3.h>
#include <stdexcept>
#include <sstream>
#include <ctime>
std::string generateSecureToken(size_t numBytes) {
std::ifstream urandom("/dev/urandom", std::ios::binary);
if (!urandom) {
throw std::runtime_error("couldn't open /dev/urandom");
}
std::vector<unsigned char> buf(numBytes);
urandom.read(reinterpret_cast<char*>(buf.data()), numBytes);
if (!urandom) {
throw std::runtime_error("short read from /dev/urandom");
}
std::stringstream ss;
for (unsigned char b : buf) {
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(b);
}
return ss.str();
}
Database::Database(const std::string& path) {
// init db
@@ -15,7 +39,8 @@ Database::Database(const std::string& path) {
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
password TEXT NOT NULL
password TEXT NOT NULL,
bio TEXT
);
CREATE TABLE IF NOT EXISTS posts (
@@ -25,6 +50,25 @@ Database::Database(const std::string& path) {
timestamp INTEGER NOT NULL,
likes INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
userid INTEGER NOT NULL,
expiry INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS likes (
postid INTEGER NOT NULL,
userid INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY,
senderid INTEGER NOT NULL,
recieverid INTEGER NOT NULL,
timestamp INTEGER NOT NULL,
text TEXT NOT NULL
);
)";
char* errmsg = nullptr;
@@ -72,7 +116,7 @@ std::optional<Post> Database::getPost(uint64_t id) {
std::vector<Post> Database::getUserPosts(uint64_t userId) {
std::stringstream sql;
sql << "SELECT * FROM posts WHERE userid = " << userId << ";";
sql << "SELECT * FROM posts WHERE userid = " << userId << " ORDER BY timestamp DESC LIMIT 100;";
std::string sqlstr = sql.str();
sqlite3_stmt* stmt;
@@ -155,13 +199,9 @@ void Database::addPost(const Post& post) {
}
void Database::addLike(uint64_t postId, uint64_t userId) {
// check if the post has already been liked
std::stringstream sql;
sql << R"(
UPDATE posts
SET likes = COALESCE(likes, 0) + 1
WHERE id =
)";
sql << postId << ";";
sql << "SELECT * FROM likes WHERE postid = " << postId << " AND userId = " << userId << ";";
std::string sqlstr = sql.str();
sqlite3_stmt* stmt;
@@ -169,12 +209,67 @@ void Database::addLike(uint64_t postId, uint64_t userId) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
// the user has UNLIKED something??? impossible
sqlite3_finalize(stmt);
// get rid of their like, how sad
sql.str("");
sql.clear();
sql << "UPDATE posts SET likes = COALESCE(likes, 1) - 1 WHERE id = " << postId << ";";
sqlstr = sql.str();
if (sqlite3_prepare_v2(db, sqlstr.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_finalize(stmt);
// remove like from the likes table :(
sql.str("");
sql.clear();
sql << "DELETE FROM likes WHERE postid = " << postId << " AND userid = " << userId << ";";
sqlstr = sql.str();
if (sqlite3_prepare_v2(db, sqlstr.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_finalize(stmt);
return;
}
sqlite3_finalize(stmt);
// add like to the post
sql.str("");
sql.clear();
sql << "UPDATE posts SET likes = COALESCE(likes, 0) + 1 WHERE id = " << postId << ";";
sqlstr = sql.str();
if (sqlite3_prepare_v2(db, sqlstr.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_finalize(stmt);
// add like to the likes table
sql.str("");
sql.clear();
sql << "INSERT INTO likes (postid, userid) VALUES (" << postId << ", " << userId << ");";
sqlstr = sql.str();
if (sqlite3_prepare_v2(db, sqlstr.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_finalize(stmt);
}
@@ -198,10 +293,17 @@ std::optional<User> Database::getUser(uint64_t id) {
std::string name{reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1))};
std::string password{reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2))};
const char* bioText = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
std::string bio;
if (bioText == NULL) {
bio = "";
} else {
bio = std::string(bioText);
}
sqlite3_finalize(stmt);
return User(id, name, password);
return User(id, name, password, bio);
}
std::optional<User> Database::getUserByName(const std::string& name) {
@@ -222,9 +324,71 @@ std::optional<User> Database::getUserByName(const std::string& name) {
uint64_t id = sqlite3_column_int64(stmt, 0);
std::string password{reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2))};
const char* bioText = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
std::string bio;
if (bioText == NULL) {
bio = "";
} else {
bio = std::string(bioText);
}
sqlite3_finalize(stmt);
return User(id, name, password);
return User(id, name, password, bio);
}
std::optional<std::string> Database::createNewToken(uint64_t id) {
std::string token = generateSecureToken(32);
int64_t expiry = static_cast<int64_t>(std::time(nullptr)) + 60 * 60 * 24 * 30; // 30 days
const char* sql = R"(
INSERT INTO sessions (token, userid, expiry)
VALUES (?, ?, ?);
)";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_bind_text(stmt, 1, token.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 2, id);
sqlite3_bind_int64(stmt, 3, expiry);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_finalize(stmt);
return token;
}
std::optional<User> Database::getUserByToken(const std::string& token) {
const char* sql = "SELECT userid, expiry FROM sessions WHERE token = ?;";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_bind_text(stmt, 1, token.c_str(), -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_DONE) {
sqlite3_finalize(stmt);
return {};
}
uint64_t userid = sqlite3_column_int64(stmt, 0);
int64_t expiry = sqlite3_column_int64(stmt, 1);
sqlite3_finalize(stmt);
if (expiry < static_cast<int64_t>(std::time(nullptr))) {
// expired; not bothering to delete it here, an expiry sweep
// job would be a reasonable thing to add later
return {};
}
return getUser(userid);
}
void Database::addUser(User& user) {
@@ -248,3 +412,114 @@ void Database::addUser(User& user) {
sqlite3_finalize(stmt);
}
void Database::updateUser(const User& user) {
const char* sql = R"(
UPDATE users
SET
name = ?,
bio = ?,
password = ?
WHERE id = ?;
)";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_bind_text(stmt, 1, user.name.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, user.bio.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, user.passwordHash.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 4, user.id);
if (sqlite3_step(stmt) != SQLITE_DONE) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_finalize(stmt);
}
void Database::invalidateUserSessions(const User& user) {
const char* sql = R"(
DELETE FROM sessions WHERE userid = ?;
)";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_bind_int64(stmt, 1, user.id);
if (sqlite3_step(stmt) != SQLITE_DONE) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_finalize(stmt);
}
std::vector<Message> Database::getMessages(uint64_t userA, uint64_t userB, uint64_t amount) {
const char* sql = R"(
SELECT * FROM messages
WHERE (senderid = ? AND recieverid = ?) OR (senderid = ? AND recieverid = ?)
ORDER BY timestamp DESC
LIMIT ?;
)";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_bind_int64(stmt, 1, userA);
sqlite3_bind_int64(stmt, 2, userB);
sqlite3_bind_int64(stmt, 3, userB);
sqlite3_bind_int64(stmt, 4, userA);
sqlite3_bind_int64(stmt, 5, amount);
std::vector<Message> messages = {};
while (sqlite3_step(stmt) == SQLITE_ROW) {
uint64_t id = sqlite3_column_int64(stmt, 0);
uint64_t senderId = sqlite3_column_int64(stmt, 1);
uint64_t recieverId = sqlite3_column_int64(stmt, 2);
std::time_t timestamp = sqlite3_column_int64(stmt, 3);
const char* textptr = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4));
std::string text;
if (textptr == NULL) {
text = "";
} else {
text = std::string(textptr);
}
messages.emplace_back(id, senderId, recieverId, text, timestamp);
}
sqlite3_finalize(stmt);
return messages;
}
void Database::addMessage(Message& message) {
const char* sql = R"(
INSERT INTO messages (senderid, recieverid, timestamp, text)
VALUES (?, ?, ?, ?);
)";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_bind_int64(stmt, 1, message.sender);
sqlite3_bind_int64(stmt, 2, message.reciever);
sqlite3_bind_int64(stmt, 3, message.timestamp);
sqlite3_bind_text(stmt, 4, message.content.c_str(), -1, SQLITE_STATIC);
if (sqlite3_step(stmt) != SQLITE_DONE) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_finalize(stmt);
}

View File

@@ -5,24 +5,48 @@
#include <optional>
#include <string>
#include <sqlite3.h>
#include <sys/types.h>
#include <vector>
#include "post.h"
void sanitize(std::string& str);
std::string generateSecureToken(size_t numBytes);
struct User {
uint64_t id = 0;
std::string name = "";
std::string passwordHash = "";
std::string bio = "";
User(uint64_t id, std::string namein, const std::string& passwordHash) :
id(id), passwordHash(passwordHash) {
User(uint64_t id, const std::string& namein, const std::string& passwordHash, const std::string& bio) :
id(id), passwordHash(passwordHash), bio(bio), name(namein) {}
User(uint64_t id, std::string namein, const std::string& passwordHash, const std::string& bio, int doSanitize) :
id(id), passwordHash(passwordHash), bio(bio) {
sanitize(namein);
name = namein;
}
};
struct Message {
uint64_t id = 0;
uint64_t sender = 0;
uint64_t reciever = 0;
std::time_t timestamp;
std::string content = "";
Message(uint64_t id, uint64_t sender, uint64_t reciever, const std::string& content, std::time_t timestamp) :
id(id), sender(sender), reciever(reciever), content(content), timestamp(timestamp) {}
Message(uint64_t id, uint64_t sender, uint64_t reciever, std::string contentin, std::time_t timestamp, int doSanitize) :
id(id), sender(sender), reciever(reciever), timestamp(timestamp) {
sanitize(contentin);
content = contentin;
}
};
class Database {
sqlite3* db;
@@ -41,9 +65,17 @@ class Database {
std::optional<User> getUser(uint64_t id);
std::optional<User> getUserByName(const std::string& name);
std::optional<User> getUserByToken(const std::string& token);
std::optional<std::string> createNewToken(uint64_t id);
// this will modify the user to have their user ID
void addUser(User& user);
void updateUser(const User& user);
void invalidateUserSessions(const User& user);
std::vector<Message> getMessages(uint64_t userA, uint64_t userB, uint64_t amount);
// modifies the message to have it's id
void addMessage(Message& message);
};

25526
server/src/json/json.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,20 @@
#include <cstdint>
#include <ctime>
#include <mutex>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
#include "httplib/httplib.h"
#include "bcrypt/bcrypt.h"
#define JSON_USE_IMPLICIT_CONVERSIONS 0
#include "json/json.hpp"
using Json = nlohmann::json;
#include "db.h"
#include "post.h"
@@ -15,42 +22,194 @@
#include "generated/header_top.h"
#include "generated/header_bottom.h"
#include "generated/footer.h"
#include "generated/login.h"
#include "generated/settings.h"
#include "generated/friends.h"
#include "generated/script_friends.h"
#include "generated/style.h"
#include "generated/style_friends.h"
#include "generated/script.h"
#include "generated/notify.h"
std::optional<User> getLoggedInUser(const httplib::Request& request, Database& database) {
if (!request.has_header("Cookie")) {
return {};
}
std::string cookieHeader = request.get_header_value("Cookie");
const std::string key = "session=";
size_t pos = cookieHeader.find(key);
if (pos == std::string::npos) {
return {};
}
pos += key.length();
size_t end = cookieHeader.find(';', pos);
std::string token = cookieHeader.substr(pos, end == std::string::npos ? std::string::npos : end - pos);
return database.getUserByToken(token);
}
int main() {
const bin2cpp::File& headerTopFile = bin2cpp::getHeader_topHtmlFile();
const bin2cpp::File& headerBottomFile = bin2cpp::getHeader_bottomHtmlFile();
const bin2cpp::File& footerfile = bin2cpp::getFooterHtmlFile();
const bin2cpp::File& loginfile = bin2cpp::getLoginHtmlFile();
const bin2cpp::File& settingsfile = bin2cpp::getSettingsHtmlFile();
const bin2cpp::File& friendsfile = bin2cpp::getFriendsHtmlFile();
const bin2cpp::File& friendsjsfile = bin2cpp::getScript_friendsJsFile();
const bin2cpp::File& e404file = bin2cpp::getE404HtmlFile();
const bin2cpp::File& stylefile = bin2cpp::getStyleCssFile();
const bin2cpp::File& friendscssfile = bin2cpp::getStyle_friendsCssFile();
const bin2cpp::File& scriptfile = bin2cpp::getScriptJsFile();
const bin2cpp::File& notifyfile = bin2cpp::getNotifyOpusFile();
std::string headerTop{headerTopFile.getBuffer(), headerTopFile.getSize()};
std::string headerBottom{headerBottomFile.getBuffer(), headerBottomFile.getSize()};
std::string footer{footerfile.getBuffer(), footerfile.getSize()};
std::string login{loginfile.getBuffer(), loginfile.getSize()};
std::string settings{settingsfile.getBuffer(), settingsfile.getSize()};
std::string friends{friendsfile.getBuffer(), friendsfile.getSize()};
std::string friendsjs{friendsjsfile.getBuffer(), friendsjsfile.getSize()};
std::string friendscss{friendscssfile.getBuffer(), friendscssfile.getSize()};
std::string e404{e404file.getBuffer(), e404file.getSize()};
std::string style{stylefile.getBuffer(), stylefile.getSize()};
std::string script{scriptfile.getBuffer(), scriptfile.getSize()};
std::string notify{notifyfile.getBuffer(), notifyfile.getSize()};
Database database{"chookchat.db"};
std::mutex data_mutex;
std::unordered_map<uint64_t, httplib::ws::WebSocket*> connectedFriends;
httplib::Server svr;
svr.Get("/style.css", [&style](const httplib::Request& request, httplib::Response& response) {
response.set_content(style, "text/css");
});
svr.Get("/style_friends.css", [&friendscss](const httplib::Request& request, httplib::Response& response) {
response.set_content(friendscss, "text/css");
});
svr.Get("/script.js", [&script](const httplib::Request& request, httplib::Response& response) {
response.set_content(script, "text/css");
response.set_content(script, "text/javascript");
});
svr.Get("/script_friends.js", [&friendsjs](const httplib::Request& request, httplib::Response& response) {
response.set_content(friendsjs, "text/javascript");
});
svr.Get("/notify.opus", [&notify](const httplib::Request& request, httplib::Response& response) {
response.set_content(notify, "audio/ogg; codecs=opus");
});
svr.Get("/login", [&login](const httplib::Request& request, httplib::Response& response) {
response.set_content(login, "text/html");
});
svr.Get("/settings", [&settings](const httplib::Request& request, httplib::Response& response) {
response.set_content(settings, "text/html");
});
svr.Get("/friends", [&friends](const httplib::Request& request, httplib::Response& response) {
response.set_content(friends, "text/html");
});
svr.Post("/login", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::string username = request.form.get_field("username");
std::string password = request.form.get_field("password");
std::lock_guard<std::mutex> lock(data_mutex);
try {
std::optional<User> user = database.getUserByName(username);
if (user.has_value()) {
if (!bcrypt::validatePassword(password, user->passwordHash)) {
response.status = 401;
response.set_content("<p>wrong password lmao</p><img src='https://media.tenor.com/wWX7upr7SvwAAAAM/byuntear-cat.gif' alt='your stupid lol'>", "text/html");
return;
}
} else {
response.status = 400;
response.set_content("<p>that username doesn't exist</p>", "text/html");
return;
}
std::optional<std::string> token = database.createNewToken(user->id);
if (!token.has_value()) {
response.status = 500;
response.set_content("<p>couldn't create a session, sorry</p>", "text/html");
return;
}
// HttpOnly so script.js can't read/leak it, SameSite=Lax so it
// isn't sent on cross-site POSTs (basic CSRF mitigation),
// Max-Age matches the 30 day expiry stored in the DB
response.set_header(
"Set-Cookie",
"session=" + *token + "; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000"
);
response.set_redirect("/");
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
svr.Post("/register", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::string username = request.form.get_field("username");
std::string password = request.form.get_field("password");
std::lock_guard<std::mutex> lock(data_mutex);
try {
std::optional<User> user = database.getUserByName(username);
if (user.has_value()) {
response.status = 400;
response.set_content("<p>that username already exists</p>", "text/html");
return;
} else {
if (username.empty()) {
response.status = 400;
response.set_content("<p>hey you can't have an empty username!!!!1!!!1! >:(</p>", "text/html");
return;
}
User newUser{0, username, bcrypt::generateHash(password), "", 1};
database.addUser(newUser);
user = newUser;
}
std::optional<std::string> token = database.createNewToken(user->id);
if (!token.has_value()) {
response.status = 500;
response.set_content("<p>couldn't create a session, sorry</p>", "text/html");
return;
}
// HttpOnly so script.js can't read/leak it, SameSite=Lax so it
// isn't sent on cross-site POSTs (basic CSRF mitigation),
// Max-Age matches the 30 day expiry stored in the DB
response.set_header(
"Set-Cookie",
"session=" + *token + "; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000"
);
response.set_redirect("/");
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
svr.Get("/", [&headerTop, &headerBottom, &footer, &database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::stringstream ss;
ss << headerTop << "<meta content='Chookchat' property='og:title' /><meta content='See posts from cool people' property='og:description' />" << headerBottom;
ss << "<div id='posts-header'><h1>posts</h1></div>";
std::lock_guard<std::mutex> lock(data_mutex);
@@ -68,11 +227,15 @@ int main() {
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
svr.Get("/posts/:id", [&headerTop, &headerBottom, &footer, &e404, &database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::string postId = request.path_params.at("id");
std::lock_guard<std::mutex> lock(data_mutex);
try {
uint64_t postIdNum = std::stoll(postId);
std::optional<Post> post = database.getPost(postIdNum);
@@ -95,6 +258,38 @@ int main() {
}
});
svr.Get("/profile/:username", [&headerTop, &headerBottom, &footer, &e404, &database, &data_mutex](const httplib::Request& request, httplib::Response& response){
std::string username = request.path_params.at("username");
std::lock_guard<std::mutex> lock(data_mutex);
try {
std::optional<User> user = database.getUserByName(username);
if (!user.has_value()) {
response.status = 404;
response.set_content(e404, "text/html");
}
std::vector<Post> posts = database.getUserPosts(user->id);
std::stringstream ss;
ss << headerTop << "<meta content='See posts from " << username << " on Chookchat' property='og:title' />" << headerBottom;
ss << "<div id='posts-header'><h1>" << username << "</h1></div>";
ss << "<p>" << user->bio << "</p>";
for (const auto& post : posts) {
ss << post.genHtml();
}
ss << footer;
response.set_content(ss.str(), "text/html");
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
// endpoint to be used by HTML forms
svr.Post("/make_post", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::string username = request.get_param_value("username");
@@ -119,7 +314,7 @@ int main() {
return;
}
// create user
User newUser{0, username, bcrypt::generateHash(password)};
User newUser{0, username, bcrypt::generateHash(password), ""};
database.addUser(newUser);
userId = newUser.id;
}
@@ -134,16 +329,371 @@ int main() {
});
// endpoint to be used in Javascript
svr.Post("/post", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
try {
std::lock_guard<std::mutex> lock(data_mutex);
std::string post = request.form.get_field("post");
std::optional<User> user = getLoggedInUser(request, database);
if (!user.has_value()) {
response.status = 401;
response.set_content("<p>you're not logged in, so you can't post</p>", "text/html");
return;
}
database.addPost(Post(post, user->id));
response.status = 200;
response.set_content("OK", "text/text");
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
svr.Post("/like", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
try {
std::string username = request.form.get_field("username");
std::string password = request.form.get_field("password");
std::string post = request.form.get_field("post");
std::lock_guard<std::mutex> lock(data_mutex);
std::optional<User> user = getLoggedInUser(request, database);
if (!user.has_value()) {
response.status = 401;
response.set_content("i dunno that user", "text/plain");
return;
}
uint64_t postNum = std::stoull(post);
database.addLike(postNum, user->id);
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
svr.Get("/me", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::lock_guard<std::mutex> lock(data_mutex);
try {
std::optional<User> user = getLoggedInUser(request, database);
response.set_header("Cache-Control", "no-store");
if (!user.has_value()) {
response.set_header("X-Logged-In", "false");
return;
}
response.set_header("X-Logged-In", "true");
response.set_header("X-Username", user->name);
response.set_header("X-Bio", user->bio);
response.set_header("X-User-ID", std::to_string(user->id));
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
svr.Get("/userinfo/:userid", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::lock_guard<std::mutex> lock(data_mutex);
try {
uint64_t userId = std::stoull(request.path_params.at("userid"));
std::optional<User> user = database.getUser(userId);
response.set_header("Cache-Control", "no-store");
if (!user.has_value()) {
response.status = 404;
return;
}
response.set_header("X-Logged-In", "true");
response.set_header("X-Username", user->name);
response.set_header("X-Bio", user->bio);
response.set_header("X-User-ID", std::to_string(user->id));
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
// friends endpoints
svr.WebSocket("/friends/ws", [&database, &data_mutex, &connectedFriends](const httplib::Request& request, httplib::ws::WebSocket& ws) {
std::optional<User> user = getLoggedInUser(request, database);
if (!user.has_value()) {
ws.close();
return;
}
// add us to the logged in users
connectedFriends[user->id] = &ws;
{
// send out an update to everyone with the new user list
Json data;
data["type"] = "users";
Json list;
for (const auto& [user, value] : connectedFriends) {
list.push_back(user);
}
data["content"] = list;
std::string datadump = data.dump();
for (const auto& [userid, conn] : connectedFriends) {
conn->send(datadump);
}
}
std::string msg;
while (ws.read(msg)) {
try {
/*
* type:
* 0 (message), 1 (friend request), 2 (change status), 3 (ping),
* 4 (get online friends)
* userId: <int>
* content:
* <string> (message),
* (0 (offline), 1 (online) (change status)),
* nil, (friend request)
*/
Json data = Json::parse(msg);
double type = data["type"].get<double>();
uint64_t userId = static_cast<uint64_t>(data["userId"].get<double>());
if (type == 0) { // message
std::string content = data["content"].get<std::string>();
if (connectedFriends.find(userId) == connectedFriends.end()) {
Json data;
data["type"] = "error";
data["content"] = "friend not online";
ws.send(data.dump());
continue;
}
httplib::ws::WebSocket* friendWs = connectedFriends[userId];
if (friendWs->is_open()) {
Json data;
data["type"] = "message";
data["userId"] = user->id;
data["content"] = content;
friendWs->send(data.dump());
Json returnData;
returnData["type"] = "ok";
ws.send(returnData.dump());
}
// Store the message in database
{
std::lock_guard<std::mutex> lock(data_mutex);
Message message{0, user->id, userId, content, std::time(nullptr), 1};
database.addMessage(message);
}
// TODO:
// - Verify users are friends
} else if (type == 1) { // friend request
Json data;
data["type"] = "error";
data["content"] = "not implemented yet";
ws.send(data.dump());
continue;
} else if (type == 2) {
// blank for now
// no need for a ping anymore
} else if (type == 3) {
Json data;
data["type"] = "ok";
ws.send(data.dump());
} else if (type == 4) {
Json data;
data["type"] = "users";
Json list;
for (const auto& [user, value] : connectedFriends) {
list.push_back(user);
}
data["content"] = list;
ws.send(data.dump());
}
} catch (const std::runtime_error& e) {
Json data;
data["type"] = "error";
data["content"] = e.what();
ws.send(data.dump());
}
}
// clean up the connection
connectedFriends.erase(user->id);
ws.close();
// send out an update to everyone with the new user list
{
Json data;
data["type"] = "users";
Json list;
for (const auto& [user, value] : connectedFriends) {
list.push_back(user);
}
data["content"] = list;
std::string datadump = data.dump();
for (const auto& [userid, conn] : connectedFriends) {
conn->send(datadump);
}
}
});
svr.Get("/friends/chatHistory/:userid", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::string userIdStr = request.path_params.at("userid");
std::lock_guard<std::mutex> lock(data_mutex);
try {
std::optional<User> user = getLoggedInUser(request, database);
if (!user.has_value()) {
response.status = 401;
response.set_content("your session is invalid", "text/plain");
return;
}
uint64_t friendUserId = std::stoull(userIdStr);
std::vector<Message> messages = database.getMessages(user->id, friendUserId, 100);
// construct json for response
Json list;
for (auto it = messages.rbegin(); it != messages.rend(); ++it) {
Json message;
message["sender"] = it->sender;
message["reciever"] = it->reciever;
message["id"] = it->id;
message["timestamp"] = it->timestamp;
message["content"] = it->content;
list.push_back(message);
}
Json data;
data["messages"] = list;
response.set_content(data.dump(), "application/json");
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
// settings endpoints
svr.Post("/settings/changePassword", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::string oldpassword = request.form.get_field("oldpassword");
std::string newpassword = request.form.get_field("newpassword");
std::lock_guard<std::mutex> lock(data_mutex);
uint64_t postNum = std::stoull(post);
database.addLike(postNum, 0);
try {
std::optional<User> user = getLoggedInUser(request, database);
if (!user.has_value()) {
response.status = 401;
response.set_content("your session is invalid", "text/plain");
return;
}
if (!bcrypt::validatePassword(oldpassword, user->passwordHash)) {
response.status = 400;
response.set_content("invalid password", "text/plain");
return;
}
user->passwordHash = bcrypt::generateHash(newpassword);
database.updateUser(*user);
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
svr.Post("/settings/changeUsername", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::string password = request.form.get_field("password");
std::string newusername = request.form.get_field("newusername");
std::lock_guard<std::mutex> lock(data_mutex);
try {
std::optional<User> user = getLoggedInUser(request, database);
if (!user.has_value()) {
response.status = 401;
response.set_content("your session is invalid", "text/plain");
return;
}
if (!bcrypt::validatePassword(password, user->passwordHash)) {
response.status = 400;
response.set_content("invalid password", "text/plain");
return;
}
user->name = newusername;
database.updateUser(*user);
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
svr.Post("/settings/changeBio", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::string bio = request.form.get_field("bio");
std::lock_guard<std::mutex> lock(data_mutex);
try {
std::optional<User> user = getLoggedInUser(request, database);
if (!user.has_value()) {
response.status = 401;
response.set_content("your session is invalid", "text/plain");
return;
}
user->bio = bio;
database.updateUser(*user);
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
svr.Post("/settings/invalidateAllSessions", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::lock_guard<std::mutex> lock(data_mutex);
try {
std::optional<User> user = getLoggedInUser(request, database);
if (!user.has_value()) {
response.status = 401;
response.set_content("your session is invalid", "text/plain");
return;
}
database.invalidateUserSessions(*user);
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");

View File

@@ -47,7 +47,7 @@ std::string Post::genHtml() const {
ts = *localtime(&time);
strftime(buf, sizeof(buf), "%a %Y-%m-%d %H:%M:%S %Z", &ts);
ss << "<p class='postinfo'><i>" << user << "</i> posted this at " << buf << "</p>\n";
ss << "<p class='postinfo'><i><a href='/profile/" << user << "'>" << user << "</a></i> posted this at " << buf << "</p>\n";
ss << content;