#include "db.h" #ifdef VOICECAT_HAS_NET #include #include #include #include #include #include namespace voicecat::server { // ── Schema ──────────────────────────────────────────────────────────────────── static constexpr int kCurrentSchemaVersion = 2; static constexpr const char* kCreateSchemaV1 = R"sql( CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, pw_hash TEXT NOT NULL, is_admin INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, last_login INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS server_meta ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); )sql"; static constexpr const char* kCreateSchemaV2 = R"sql( CREATE TABLE IF NOT EXISTS channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, parent_id INTEGER NOT NULL DEFAULT 0, name TEXT UNIQUE NOT NULL, topic TEXT NOT NULL DEFAULT '', password_hash TEXT NOT NULL DEFAULT '', max_users INTEGER NOT NULL DEFAULT 0, type INTEGER NOT NULL DEFAULT 0, audio_codec INTEGER NOT NULL DEFAULT 0, audio_mode INTEGER NOT NULL DEFAULT 0, audio_sample_rate INTEGER NOT NULL DEFAULT 48000, audio_bitrate_bps INTEGER NOT NULL DEFAULT 24000, audio_frame_ms INTEGER NOT NULL DEFAULT 20, audio_application INTEGER NOT NULL DEFAULT 0, audio_fec INTEGER NOT NULL DEFAULT 1, audio_expected_packet_loss INTEGER NOT NULL DEFAULT 10, audio_dtx INTEGER NOT NULL DEFAULT 1, audio_complexity INTEGER NOT NULL DEFAULT 5, sort_order INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS bans ( id INTEGER PRIMARY KEY AUTOINCREMENT, subject_type TEXT NOT NULL, subject TEXT NOT NULL, reason TEXT NOT NULL DEFAULT '', expires_at INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_bans_subject ON bans(subject_type, subject); )sql"; // ── Database ────────────────────────────────────────────────────────────────── Database::Database(std::string path) : path_(std::move(path)) {} Database::~Database() { if (db_) { sqlite3_close(db_); db_ = nullptr; } } bool Database::open(std::string& error) { int rc = sqlite3_open(path_.c_str(), &db_); if (rc != SQLITE_OK) { error = sqlite3_errmsg(db_); sqlite3_close(db_); db_ = nullptr; return false; } sqlite3_busy_timeout(db_, 5000); // WAL mode for concurrency exec("PRAGMA journal_mode=WAL", error); exec("PRAGMA synchronous=NORMAL", error); error.clear(); if (!exec(kCreateSchemaV1, error)) return false; if (!migrate(error)) return false; return true; } bool Database::migrate(std::string& error) { // Ensure server_meta row exists. if (!exec("INSERT OR IGNORE INTO server_meta (key, value) VALUES ('schema_version', '1')", error)) return false; sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "SELECT value FROM server_meta WHERE key='schema_version'", -1, &stmt, nullptr); int version = 1; if (sqlite3_step(stmt) == SQLITE_ROW) { version = std::max(1, std::atoi(reinterpret_cast(sqlite3_column_text(stmt, 0)))); } sqlite3_finalize(stmt); if (version < 2) { if (!exec(kCreateSchemaV2, error)) return false; sqlite3_stmt* upd = nullptr; sqlite3_prepare_v2(db_, "INSERT OR REPLACE INTO server_meta (key, value) VALUES ('schema_version', ?)", -1, &upd, nullptr); sqlite3_bind_text(upd, 1, "2", -1, SQLITE_TRANSIENT); sqlite3_step(upd); sqlite3_finalize(upd); } return true; } bool Database::is_empty() { sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "SELECT COUNT(*) FROM accounts", -1, &stmt, nullptr); int count = 0; if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); return count == 0; } // ── Accounts ────────────────────────────────────────────────────────────────── std::optional Database::create_account(const std::string& username, const std::string& password, bool is_admin, std::string& error) { if (username.empty() || password.empty()) { error = "username and password must not be empty"; return std::nullopt; } // Hash with Argon2id via libsodium char hash[crypto_pwhash_STRBYTES]; if (crypto_pwhash_str(hash, password.c_str(), password.size(), crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE) != 0) { error = "Argon2id hashing failed (OOM?)"; return std::nullopt; } int64_t now = now_unix(); sqlite3_stmt* stmt = nullptr; int rc = sqlite3_prepare_v2(db_, "INSERT INTO accounts (username, pw_hash, is_admin, created_at) VALUES (?,?,?,?)", -1, &stmt, nullptr); if (rc != SQLITE_OK) { error = sqlite3_errmsg(db_); return std::nullopt; } sqlite3_bind_text(stmt, 1, username.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, hash, -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 3, is_admin ? 1 : 0); sqlite3_bind_int64(stmt, 4, now); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { error = sqlite3_errmsg(db_); return std::nullopt; } Account acc; acc.id = sqlite3_last_insert_rowid(db_); acc.username = username; acc.is_admin = is_admin; acc.created_at = now; return acc; } bool Database::reset_password(const std::string& username, const std::string& new_password, std::string& error) { if (new_password.empty()) { error = "password must not be empty"; return false; } char hash[crypto_pwhash_STRBYTES]; if (crypto_pwhash_str(hash, new_password.c_str(), new_password.size(), crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE) != 0) { error = "Argon2id hashing failed"; return false; } sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "UPDATE accounts SET pw_hash=? WHERE username=?", -1, &stmt, nullptr); sqlite3_bind_text(stmt, 1, hash, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, username.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { error = sqlite3_errmsg(db_); return false; } if (sqlite3_changes(db_) == 0) { error = "user not found: " + username; return false; } return true; } bool Database::delete_account(const std::string& username, std::string& error) { sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "DELETE FROM accounts WHERE username=?", -1, &stmt, nullptr); sqlite3_bind_text(stmt, 1, username.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { error = sqlite3_errmsg(db_); return false; } if (sqlite3_changes(db_) == 0) { error = "user not found: " + username; return false; } return true; } std::vector Database::list_accounts() { std::vector result; sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "SELECT id, username, is_admin, created_at, last_login FROM accounts ORDER BY username", -1, &stmt, nullptr); while (sqlite3_step(stmt) == SQLITE_ROW) { Account acc; acc.id = sqlite3_column_int64(stmt, 0); acc.username = reinterpret_cast(sqlite3_column_text(stmt, 1)); acc.is_admin = sqlite3_column_int(stmt, 2) != 0; acc.created_at = sqlite3_column_int64(stmt, 3); acc.last_login = sqlite3_column_int64(stmt, 4); result.push_back(acc); } sqlite3_finalize(stmt); return result; } std::optional Database::authenticate(const std::string& username, const std::string& password) { sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "SELECT id, pw_hash, is_admin, created_at, last_login FROM accounts WHERE username=?", -1, &stmt, nullptr); sqlite3_bind_text(stmt, 1, username.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); if (rc != SQLITE_ROW) { sqlite3_finalize(stmt); return std::nullopt; } int64_t id = sqlite3_column_int64(stmt, 0); std::string hash = reinterpret_cast(sqlite3_column_text(stmt, 1)); bool is_admin = sqlite3_column_int(stmt, 2) != 0; int64_t created = sqlite3_column_int64(stmt, 3); sqlite3_finalize(stmt); // Verify Argon2id — deliberately slow if (crypto_pwhash_str_verify(hash.c_str(), password.c_str(), password.size()) != 0) return std::nullopt; // Update last_login int64_t now = now_unix(); sqlite3_stmt* upd = nullptr; sqlite3_prepare_v2(db_, "UPDATE accounts SET last_login=? WHERE id=?", -1, &upd, nullptr); sqlite3_bind_int64(upd, 1, now); sqlite3_bind_int64(upd, 2, id); sqlite3_step(upd); sqlite3_finalize(upd); Account acc; acc.id = id; acc.username = username; acc.is_admin = is_admin; acc.created_at = created; acc.last_login = now; return acc; } // ── Channels ────────────────────────────────────────────────────────────────── namespace { void bind_audio(sqlite3_stmt* stmt, int base_idx, const voicecat::v1::AudioConfig& a) { sqlite3_bind_int(stmt, base_idx + 0, static_cast(a.codec())); sqlite3_bind_int(stmt, base_idx + 1, static_cast(a.mode())); sqlite3_bind_int(stmt, base_idx + 2, static_cast(a.sample_rate())); sqlite3_bind_int(stmt, base_idx + 3, static_cast(a.bitrate_bps())); sqlite3_bind_int(stmt, base_idx + 4, static_cast(a.frame_ms())); sqlite3_bind_int(stmt, base_idx + 5, static_cast(a.application())); sqlite3_bind_int(stmt, base_idx + 6, a.fec() ? 1 : 0); sqlite3_bind_int(stmt, base_idx + 7, static_cast(a.expected_packet_loss())); sqlite3_bind_int(stmt, base_idx + 8, a.dtx() ? 1 : 0); sqlite3_bind_int(stmt, base_idx + 9, static_cast(a.complexity())); } voicecat::v1::AudioConfig read_audio(sqlite3_stmt* stmt, int base_idx) { voicecat::v1::AudioConfig a; a.set_codec(static_cast(sqlite3_column_int(stmt, base_idx + 0))); a.set_mode(static_cast(sqlite3_column_int(stmt, base_idx + 1))); a.set_sample_rate(static_cast(sqlite3_column_int(stmt, base_idx + 2))); a.set_bitrate_bps(static_cast(sqlite3_column_int(stmt, base_idx + 3))); a.set_frame_ms(static_cast(sqlite3_column_int(stmt, base_idx + 4))); a.set_application(static_cast(sqlite3_column_int(stmt, base_idx + 5))); a.set_fec(sqlite3_column_int(stmt, base_idx + 6) != 0); a.set_expected_packet_loss(static_cast(sqlite3_column_int(stmt, base_idx + 7))); a.set_dtx(sqlite3_column_int(stmt, base_idx + 8) != 0); a.set_complexity(static_cast(sqlite3_column_int(stmt, base_idx + 9))); return a; } } // namespace std::optional Database::create_channel(const ChannelRecord& ch, const std::string& password, std::string& error) { if (ch.name.empty()) { error = "channel name must not be empty"; return std::nullopt; } std::string pw_hash = password.empty() ? "" : hash_channel_password(password); sqlite3_stmt* stmt = nullptr; const char* sql = R"sql( INSERT INTO channels (parent_id, name, topic, password_hash, max_users, type, audio_codec, audio_mode, audio_sample_rate, audio_bitrate_bps, audio_frame_ms, audio_application, audio_fec, audio_expected_packet_loss, audio_dtx, audio_complexity, sort_order) VALUES (?,?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?) )sql"; int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { error = sqlite3_errmsg(db_); return std::nullopt; } sqlite3_bind_int(stmt, 1, static_cast(ch.parent_id)); sqlite3_bind_text(stmt, 2, ch.name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, ch.topic.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 4, pw_hash.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 5, static_cast(ch.max_users)); sqlite3_bind_int(stmt, 6, static_cast(ch.type)); bind_audio(stmt, 7, ch.audio); // parameters 7-16 sqlite3_bind_int(stmt, 17, static_cast(ch.sort_order)); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { error = sqlite3_errmsg(db_); return std::nullopt; } ChannelRecord out = ch; out.id = static_cast(sqlite3_last_insert_rowid(db_)); out.password_protected = !pw_hash.empty(); return out; } bool Database::update_channel(const ChannelRecord& ch, const std::string& password, std::string& error) { std::string pw_hash; bool update_password = false; if (!password.empty()) { pw_hash = hash_channel_password(password); update_password = true; } std::string sql = R"sql( UPDATE channels SET parent_id=?, name=?, topic=?, max_users=?, type=?, audio_codec=?, audio_mode=?, audio_sample_rate=?, audio_bitrate_bps=?, audio_frame_ms=?, audio_application=?, audio_fec=?, audio_expected_packet_loss=?, audio_dtx=?, audio_complexity=?, sort_order=? )sql"; if (update_password) sql += ", password_hash=?"; sql += " WHERE id=?"; sqlite3_stmt* stmt = nullptr; int rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr); if (rc != SQLITE_OK) { error = sqlite3_errmsg(db_); return false; } int idx = 1; sqlite3_bind_int(stmt, idx++, static_cast(ch.parent_id)); sqlite3_bind_text(stmt, idx++, ch.name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, idx++, ch.topic.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, idx++, static_cast(ch.max_users)); sqlite3_bind_int(stmt, idx++, static_cast(ch.type)); bind_audio(stmt, idx, ch.audio); idx += 10; sqlite3_bind_int(stmt, idx++, static_cast(ch.sort_order)); if (update_password) { sqlite3_bind_text(stmt, idx++, pw_hash.c_str(), -1, SQLITE_TRANSIENT); } sqlite3_bind_int(stmt, idx++, static_cast(ch.id)); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { error = sqlite3_errmsg(db_); return false; } if (sqlite3_changes(db_) == 0) { error = "channel not found"; return false; } return true; } bool Database::delete_channel(uint32_t id, std::string& error) { sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "DELETE FROM channels WHERE id=?", -1, &stmt, nullptr); sqlite3_bind_int(stmt, 1, static_cast(id)); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { error = sqlite3_errmsg(db_); return false; } if (sqlite3_changes(db_) == 0) { error = "channel not found"; return false; } return true; } std::vector Database::list_channels() { std::vector result; sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, R"sql( SELECT id, parent_id, name, topic, password_hash, max_users, type, audio_codec, audio_mode, audio_sample_rate, audio_bitrate_bps, audio_frame_ms, audio_application, audio_fec, audio_expected_packet_loss, audio_dtx, audio_complexity, sort_order FROM channels ORDER BY sort_order, id )sql", -1, &stmt, nullptr); while (sqlite3_step(stmt) == SQLITE_ROW) { ChannelRecord ch; ch.id = static_cast(sqlite3_column_int(stmt, 0)); ch.parent_id = static_cast(sqlite3_column_int(stmt, 1)); ch.name = reinterpret_cast(sqlite3_column_text(stmt, 2)); ch.topic = reinterpret_cast(sqlite3_column_text(stmt, 3)); ch.password_protected = sqlite3_column_text(stmt, 4)[0] != '\0'; ch.max_users = static_cast(sqlite3_column_int(stmt, 5)); ch.type = static_cast(sqlite3_column_int(stmt, 6)); ch.audio = read_audio(stmt, 7); ch.sort_order = sqlite3_column_int(stmt, 17); result.push_back(std::move(ch)); } sqlite3_finalize(stmt); return result; } std::optional Database::get_channel(uint32_t id) { sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, R"sql( SELECT id, parent_id, name, topic, password_hash, max_users, type, audio_codec, audio_mode, audio_sample_rate, audio_bitrate_bps, audio_frame_ms, audio_application, audio_fec, audio_expected_packet_loss, audio_dtx, audio_complexity, sort_order FROM channels WHERE id=? )sql", -1, &stmt, nullptr); sqlite3_bind_int(stmt, 1, static_cast(id)); std::optional result; if (sqlite3_step(stmt) == SQLITE_ROW) { ChannelRecord ch; ch.id = static_cast(sqlite3_column_int(stmt, 0)); ch.parent_id = static_cast(sqlite3_column_int(stmt, 1)); ch.name = reinterpret_cast(sqlite3_column_text(stmt, 2)); ch.topic = reinterpret_cast(sqlite3_column_text(stmt, 3)); ch.password_protected = sqlite3_column_text(stmt, 4)[0] != '\0'; ch.max_users = static_cast(sqlite3_column_int(stmt, 5)); ch.type = static_cast(sqlite3_column_int(stmt, 6)); ch.audio = read_audio(stmt, 7); ch.sort_order = sqlite3_column_int(stmt, 17); result = std::move(ch); } sqlite3_finalize(stmt); return result; } std::string Database::hash_channel_password(const std::string& password) { uint8_t salt[16]; randombytes_buf(salt, sizeof(salt)); uint8_t hash[32]; crypto_generichash(hash, sizeof(hash), reinterpret_cast(password.data()), password.size(), salt, sizeof(salt)); char salt_hex[33]; char hash_hex[65]; sodium_bin2hex(salt_hex, sizeof(salt_hex), salt, sizeof(salt)); sodium_bin2hex(hash_hex, sizeof(hash_hex), hash, sizeof(hash)); return std::string(salt_hex) + ":" + hash_hex; } bool Database::verify_channel_password(const std::string& password, const std::string& stored) { auto pos = stored.find(':'); if (pos == std::string::npos || pos != 32) return false; std::string salt_hex = stored.substr(0, pos); std::string hash_hex = stored.substr(pos + 1); uint8_t salt[16]; uint8_t expected[32]; if (sodium_hex2bin(salt, sizeof(salt), salt_hex.c_str(), salt_hex.size(), nullptr, nullptr, nullptr) != 0) return false; if (sodium_hex2bin(expected, sizeof(expected), hash_hex.c_str(), hash_hex.size(), nullptr, nullptr, nullptr) != 0) return false; uint8_t hash[32]; crypto_generichash(hash, sizeof(hash), reinterpret_cast(password.data()), password.size(), salt, sizeof(salt)); return sodium_memcmp(hash, expected, sizeof(hash)) == 0; } bool Database::check_channel_password(uint32_t id, const std::string& password) { sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "SELECT password_hash FROM channels WHERE id=?", -1, &stmt, nullptr); sqlite3_bind_int(stmt, 1, static_cast(id)); bool ok = false; if (sqlite3_step(stmt) == SQLITE_ROW) { const char* stored = reinterpret_cast(sqlite3_column_text(stmt, 0)); if (!stored || stored[0] == '\0') { ok = true; // no password } else { ok = verify_channel_password(password, stored); } } sqlite3_finalize(stmt); return ok; } // ── Bans ─────────────────────────────────────────────────────────────────────── std::optional Database::ban_create(const std::string& subject_type, const std::string& subject, const std::string& reason, int64_t expires_at, std::string& error) { if (subject_type != "user_id" && subject_type != "username" && subject_type != "ip") { error = "invalid subject_type"; return std::nullopt; } if (subject.empty()) { error = "subject must not be empty"; return std::nullopt; } int64_t now = now_unix(); sqlite3_stmt* stmt = nullptr; int rc = sqlite3_prepare_v2(db_, "INSERT INTO bans (subject_type, subject, reason, expires_at, created_at) VALUES (?,?,?,?,?)", -1, &stmt, nullptr); if (rc != SQLITE_OK) { error = sqlite3_errmsg(db_); return std::nullopt; } sqlite3_bind_text(stmt, 1, subject_type.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, subject.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, reason.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 4, expires_at); sqlite3_bind_int64(stmt, 5, now); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { error = sqlite3_errmsg(db_); return std::nullopt; } BanRecord b; b.id = sqlite3_last_insert_rowid(db_); b.subject_type = subject_type; b.subject = subject; b.reason = reason; b.expires_at = expires_at; b.created_at = now; return b; } bool Database::ban_remove(int64_t id, std::string& error) { sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "DELETE FROM bans WHERE id=?", -1, &stmt, nullptr); sqlite3_bind_int64(stmt, 1, id); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { error = sqlite3_errmsg(db_); return false; } if (sqlite3_changes(db_) == 0) { error = "ban not found"; return false; } return true; } bool Database::ban_check(const std::string& subject_type, const std::string& subject) { sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "SELECT expires_at FROM bans WHERE subject_type=? AND subject=? AND (expires_at=0 OR expires_at>?)", -1, &stmt, nullptr); sqlite3_bind_text(stmt, 1, subject_type.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, subject.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 3, now_unix()); bool banned = false; if (sqlite3_step(stmt) == SQLITE_ROW) banned = true; sqlite3_finalize(stmt); return banned; } std::vector Database::ban_list(bool include_expired) { std::vector result; sqlite3_stmt* stmt = nullptr; std::string sql = "SELECT id, subject_type, subject, reason, expires_at, created_at FROM bans"; if (!include_expired) sql += " WHERE expires_at=0 OR expires_at>?"; sql += " ORDER BY created_at DESC"; sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr); if (!include_expired) sqlite3_bind_int64(stmt, 1, now_unix()); while (sqlite3_step(stmt) == SQLITE_ROW) { BanRecord b; b.id = sqlite3_column_int64(stmt, 0); b.subject_type = reinterpret_cast(sqlite3_column_text(stmt, 1)); b.subject = reinterpret_cast(sqlite3_column_text(stmt, 2)); b.reason = reinterpret_cast(sqlite3_column_text(stmt, 3)); b.expires_at = sqlite3_column_int64(stmt, 4); b.created_at = sqlite3_column_int64(stmt, 5); result.push_back(std::move(b)); } sqlite3_finalize(stmt); return result; } std::string Database::generate_password(size_t length) { static const char kAlphabet[] = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$%^&*"; constexpr size_t kAlphaLen = sizeof(kAlphabet) - 1; std::string pw; pw.reserve(length); for (size_t i = 0; i < length; ++i) { uint8_t rnd[1]; randombytes_buf(rnd, 1); pw += kAlphabet[rnd[0] % kAlphaLen]; } return pw; } bool Database::exec(const std::string& sql, std::string& error) { char* errmsg = nullptr; int rc = sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &errmsg); if (rc != SQLITE_OK) { error = errmsg ? errmsg : "unknown error"; if (errmsg) sqlite3_free(errmsg); return false; } return true; } int64_t Database::now_unix() const { using namespace std::chrono; return duration_cast(system_clock::now().time_since_epoch()).count(); } } // namespace voicecat::server #endif // VOICECAT_HAS_NET