526 lines
17 KiB
C++
526 lines
17 KiB
C++
#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
|
|
if (sqlite3_open(path.c_str(), &db) != SQLITE_OK) {
|
|
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
|
|
}
|
|
|
|
// create tables in case they don't exist
|
|
const char* sql = R"(
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
password TEXT NOT NULL,
|
|
bio TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS posts (
|
|
id INTEGER PRIMARY KEY,
|
|
userid INTEGER NOT NULL,
|
|
content TEXT NOT NULL,
|
|
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;
|
|
|
|
if (sqlite3_exec(db, sql, nullptr, nullptr, &errmsg) != SQLITE_OK) {
|
|
throw std::runtime_error("sqlite3 error: " + std::string(errmsg));
|
|
}
|
|
}
|
|
|
|
Database::~Database() {
|
|
sqlite3_close(db);
|
|
}
|
|
|
|
std::optional<Post> Database::getPost(uint64_t id) {
|
|
std::stringstream sql;
|
|
sql << "SELECT * FROM posts WHERE id = " << id << ";";
|
|
std::string sqlstr = sql.str();
|
|
|
|
sqlite3_stmt* stmt;
|
|
if (sqlite3_prepare_v2(db, sqlstr.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
|
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
|
|
}
|
|
|
|
// we only want the first one (there's probably only one)
|
|
// if there is none, return nothing
|
|
if (sqlite3_step(stmt) == SQLITE_DONE) {
|
|
return {};
|
|
}
|
|
int64_t userid = sqlite3_column_int64(stmt, 1);
|
|
std::string content{reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2))};
|
|
int64_t timestamp = sqlite3_column_int64(stmt, 3);
|
|
int64_t likes = sqlite3_column_int64(stmt, 4);
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
// get user info from post
|
|
std::optional<User> user = getUser(userid);
|
|
if (user.has_value()) {
|
|
return Post(id, content, user->name, timestamp, likes);
|
|
} else {
|
|
throw std::runtime_error("post has invalid user ID attached (this is weird)");
|
|
}
|
|
|
|
}
|
|
|
|
std::vector<Post> Database::getUserPosts(uint64_t userId) {
|
|
std::stringstream sql;
|
|
sql << "SELECT * FROM posts WHERE userid = " << userId << " ORDER BY timestamp DESC LIMIT 100;";
|
|
std::string sqlstr = sql.str();
|
|
|
|
sqlite3_stmt* stmt;
|
|
if (sqlite3_prepare_v2(db, sqlstr.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
|
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
|
|
}
|
|
|
|
// get user info from post
|
|
std::optional<User> user = getUser(userId);
|
|
if (!user.has_value()) {
|
|
throw std::runtime_error("user does not exist");
|
|
}
|
|
|
|
std::vector<Post> posts = {};
|
|
|
|
while (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
int64_t postid = sqlite3_column_int64(stmt, 0);
|
|
std::string content{reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2))};
|
|
int64_t timestamp = sqlite3_column_int64(stmt, 3);
|
|
int64_t likes = sqlite3_column_int64(stmt, 4);
|
|
|
|
posts.emplace_back(postid, content, user->name, timestamp, likes);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return posts;
|
|
}
|
|
|
|
std::vector<Post> Database::getTopPosts(uint64_t amount) {
|
|
std::stringstream sql;
|
|
sql << "SELECT * FROM posts ORDER BY timestamp DESC LIMIT " << amount << ";";
|
|
std::string sqlstr = sql.str();
|
|
|
|
sqlite3_stmt* stmt;
|
|
if (sqlite3_prepare_v2(db, sqlstr.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
|
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
|
|
}
|
|
|
|
std::vector<Post> posts = {};
|
|
while (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
int64_t postid = sqlite3_column_int64(stmt, 0);
|
|
int64_t userid = sqlite3_column_int64(stmt, 1);
|
|
std::string content{reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2))};
|
|
int64_t timestamp = sqlite3_column_int64(stmt, 3);
|
|
int64_t likes = sqlite3_column_int64(stmt, 4);
|
|
|
|
// get username from id
|
|
std::optional<User> user = getUser(userid);
|
|
if (!user.has_value()) {
|
|
throw std::runtime_error("post has invalid user ID attached (this is weird)");
|
|
}
|
|
|
|
posts.emplace_back(postid, content, user->name, timestamp, likes);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return posts;
|
|
|
|
}
|
|
|
|
void Database::addPost(const Post& post) {
|
|
const char* sql = R"(
|
|
INSERT INTO posts (userid, content, timestamp)
|
|
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, post.userId);
|
|
sqlite3_bind_text(stmt, 2, post.content.c_str(), -1, SQLITE_STATIC);
|
|
sqlite3_bind_int64(stmt, 3, post.time);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
|
|
void Database::addLike(uint64_t postId, uint64_t userId) {
|
|
// check if the post has already been liked
|
|
std::stringstream sql;
|
|
sql << "SELECT * FROM likes WHERE postid = " << postId << " AND userId = " << userId << ";";
|
|
std::string sqlstr = sql.str();
|
|
|
|
sqlite3_stmt* stmt;
|
|
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) {
|
|
// 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);
|
|
}
|
|
|
|
|
|
|
|
|
|
std::optional<User> Database::getUser(uint64_t id) {
|
|
std::stringstream sql;
|
|
sql << "SELECT * FROM users WHERE id = " << id << ";";
|
|
std::string sqlstr = sql.str();
|
|
|
|
sqlite3_stmt* stmt;
|
|
if (sqlite3_prepare_v2(db, sqlstr.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
|
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
|
|
}
|
|
|
|
// we only want the first one (there's probably only one)
|
|
// if there is none, return nothing
|
|
if (sqlite3_step(stmt) == SQLITE_DONE) {
|
|
return {};
|
|
}
|
|
|
|
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, bio);
|
|
}
|
|
|
|
std::optional<User> Database::getUserByName(const std::string& name) {
|
|
std::string sql = "SELECT * FROM users WHERE name = ?;";
|
|
|
|
sqlite3_stmt* stmt;
|
|
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
|
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_STATIC);
|
|
|
|
// we only want the first one (there's probably only one)
|
|
// if there is none, return nothing
|
|
if (sqlite3_step(stmt) == SQLITE_DONE) {
|
|
return {};
|
|
}
|
|
|
|
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, 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) {
|
|
const char* sql = R"(
|
|
INSERT INTO users (name, password)
|
|
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, user.name.c_str(), -1, SQLITE_STATIC);
|
|
sqlite3_bind_text(stmt, 2, user.passwordHash.c_str(), -1, SQLITE_STATIC);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
|
|
}
|
|
|
|
user.id = sqlite3_last_insert_rowid(db);
|
|
|
|
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);
|
|
}
|