#include "db.h" #include #include #include #include #include #include #include #include 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 buf(numBytes); urandom.read(reinterpret_cast(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(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 ); 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 ); )"; 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 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(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 = 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 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 = getUser(userId); if (!user.has_value()) { throw std::runtime_error("user does not exist"); } std::vector posts = {}; while (sqlite3_step(stmt) != SQLITE_DONE) { int64_t postid = sqlite3_column_int64(stmt, 0); std::string content{reinterpret_cast(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 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 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(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 = 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 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(sqlite3_column_text(stmt, 1))}; std::string password{reinterpret_cast(sqlite3_column_text(stmt, 2))}; sqlite3_finalize(stmt); return User(id, name, password); } std::optional 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(sqlite3_column_text(stmt, 2))}; sqlite3_finalize(stmt); return User(id, name, password); } std::optional Database::createNewToken(uint64_t id) { std::string token = generateSecureToken(32); int64_t expiry = static_cast(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 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(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); }