19#include <urcu/rculfhash.h>
22static const char *schema_sql =
24 "CREATE TABLE IF NOT EXISTS sessions ("
25 " session_id BLOB PRIMARY KEY,"
26 " session_string TEXT UNIQUE NOT NULL,"
27 " host_pubkey BLOB NOT NULL,"
28 " password_hash TEXT,"
29 " max_participants INTEGER DEFAULT 4,"
30 " capabilities INTEGER DEFAULT 3,"
31 " created_at INTEGER NOT NULL,"
32 " expires_at INTEGER NOT NULL"
36 "CREATE TABLE IF NOT EXISTS participants ("
37 " participant_id BLOB PRIMARY KEY,"
38 " session_id BLOB NOT NULL,"
39 " identity_pubkey BLOB NOT NULL,"
40 " joined_at INTEGER NOT NULL,"
41 " FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE"
45 "CREATE TABLE IF NOT EXISTS rate_events ("
46 " id INTEGER PRIMARY KEY AUTOINCREMENT,"
47 " ip_address TEXT NOT NULL,"
48 " event_type TEXT NOT NULL,"
49 " timestamp INTEGER NOT NULL"
53 "CREATE INDEX IF NOT EXISTS idx_sessions_string ON sessions(session_string);"
54 "CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);"
55 "CREATE INDEX IF NOT EXISTS idx_participants_session ON participants(session_id);"
56 "CREATE INDEX IF NOT EXISTS idx_rate_events ON rate_events(ip_address, event_type, timestamp);";
59 if (!db_path || !db) {
63 log_info(
"Opening database: %s", db_path);
66 int rc = sqlite3_open(db_path, db);
67 if (rc != SQLITE_OK) {
68 const char *err = sqlite3_errmsg(*db);
76 rc = sqlite3_exec(*db,
"PRAGMA journal_mode=WAL;", NULL, NULL, &err_msg);
77 if (rc != SQLITE_OK) {
78 log_warn(
"Failed to enable WAL mode: %s", err_msg ? err_msg :
"unknown error");
79 sqlite3_free(err_msg);
83 rc = sqlite3_exec(*db,
"PRAGMA foreign_keys=ON;", NULL, NULL, &err_msg);
84 if (rc != SQLITE_OK) {
85 log_error(
"Failed to enable foreign keys: %s", err_msg ? err_msg :
"unknown error");
86 sqlite3_free(err_msg);
93 rc = sqlite3_exec(*db, schema_sql, NULL, NULL, &err_msg);
94 if (rc != SQLITE_OK) {
95 log_error(
"Failed to create schema: %s", err_msg ? err_msg :
"unknown error");
96 sqlite3_free(err_msg);
102 log_info(
"Database initialized successfully");
107 if (!db || !registry) {
113 clock_gettime(CLOCK_REALTIME, &ts);
117 const char *sql =
"SELECT session_id, session_string, host_pubkey, password_hash, "
118 "max_participants, capabilities, created_at, expires_at "
119 "FROM sessions WHERE expires_at > ?";
121 sqlite3_stmt *stmt = NULL;
122 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
123 if (rc != SQLITE_OK) {
127 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now);
129 size_t loaded_count = 0;
132 while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
136 sqlite3_finalize(stmt);
140 memset(session, 0,
sizeof(*session));
143 memcpy(session->
session_id, sqlite3_column_blob(stmt, 0), 16);
144 const char *session_string = (
const char *)sqlite3_column_text(stmt, 1);
146 memcpy(session->
host_pubkey, sqlite3_column_blob(stmt, 2), 32);
148 const char *password_hash = (
const char *)sqlite3_column_text(stmt, 3);
163 sqlite3_stmt *part_stmt = NULL;
164 const char *part_sql =
"SELECT participant_id, identity_pubkey, joined_at "
165 "FROM participants WHERE session_id = ?";
167 rc = sqlite3_prepare_v2(db, part_sql, -1, &part_stmt, NULL);
168 if (rc == SQLITE_OK) {
169 sqlite3_bind_blob(part_stmt, 1, session->
session_id, 16, SQLITE_STATIC);
172 while ((rc = sqlite3_step(part_stmt)) == SQLITE_ROW && part_idx <
MAX_PARTICIPANTS) {
175 memcpy(participant->
participant_id, sqlite3_column_blob(part_stmt, 0), 16);
176 memcpy(participant->
identity_pubkey, sqlite3_column_blob(part_stmt, 1), 32);
185 sqlite3_finalize(part_stmt);
190 unsigned long hash = 5381;
193 while ((c = (
unsigned char)*str++)) {
194 hash = ((hash << 5) + hash) + c;
198 struct cds_lfht_node *ret_node = cds_lfht_add_unique(registry->
sessions, hash,
209 log_warn(
"Duplicate session in database: %s (skipping)", session_string);
216 sqlite3_finalize(stmt);
218 if (rc != SQLITE_DONE) {
219 log_warn(
"Session loading ended with status %d: %s", rc, sqlite3_errmsg(db));
222 log_info(
"Loaded %zu sessions from database", loaded_count);
227 if (!db || !session) {
232 char *err_msg = NULL;
233 int rc = sqlite3_exec(db,
"BEGIN TRANSACTION;", NULL, NULL, &err_msg);
234 if (rc != SQLITE_OK) {
235 log_error(
"Failed to begin transaction: %s", err_msg ? err_msg :
"unknown error");
236 sqlite3_free(err_msg);
241 const char *sql =
"INSERT OR REPLACE INTO sessions "
242 "(session_id, session_string, host_pubkey, password_hash, "
243 "max_participants, capabilities, created_at, expires_at) "
244 "VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
246 sqlite3_stmt *stmt = NULL;
247 rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
248 if (rc != SQLITE_OK) {
249 sqlite3_exec(db,
"ROLLBACK;", NULL, NULL, NULL);
253 sqlite3_bind_blob(stmt, 1, session->
session_id, 16, SQLITE_STATIC);
254 sqlite3_bind_text(stmt, 2, session->
session_string, -1, SQLITE_STATIC);
255 sqlite3_bind_blob(stmt, 3, session->
host_pubkey, 32, SQLITE_STATIC);
259 sqlite3_bind_int64(stmt, 7, (sqlite3_int64)session->
created_at);
260 sqlite3_bind_int64(stmt, 8, (sqlite3_int64)session->
expires_at);
262 rc = sqlite3_step(stmt);
263 sqlite3_finalize(stmt);
265 if (rc != SQLITE_DONE) {
266 sqlite3_exec(db,
"ROLLBACK;", NULL, NULL, NULL);
271 const char *del_sql =
"DELETE FROM participants WHERE session_id = ?";
272 rc = sqlite3_prepare_v2(db, del_sql, -1, &stmt, NULL);
273 if (rc == SQLITE_OK) {
274 sqlite3_bind_blob(stmt, 1, session->
session_id, 16, SQLITE_STATIC);
276 sqlite3_finalize(stmt);
280 const char *part_sql =
"INSERT INTO participants (participant_id, session_id, identity_pubkey, joined_at) "
281 "VALUES (?, ?, ?, ?)";
285 rc = sqlite3_prepare_v2(db, part_sql, -1, &stmt, NULL);
286 if (rc != SQLITE_OK) {
291 sqlite3_bind_blob(stmt, 2, session->
session_id, 16, SQLITE_STATIC);
296 sqlite3_finalize(stmt);
301 rc = sqlite3_exec(db,
"COMMIT;", NULL, NULL, &err_msg);
302 if (rc != SQLITE_OK) {
303 log_error(
"Failed to commit transaction: %s", err_msg ? err_msg :
"unknown error");
304 sqlite3_free(err_msg);
305 sqlite3_exec(db,
"ROLLBACK;", NULL, NULL, NULL);
318 const char *sql =
"DELETE FROM sessions WHERE session_id = ?";
320 sqlite3_stmt *stmt = NULL;
321 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
322 if (rc != SQLITE_OK) {
326 sqlite3_bind_blob(stmt, 1,
session_id, 16, SQLITE_STATIC);
328 rc = sqlite3_step(stmt);
329 sqlite3_finalize(stmt);
331 if (rc != SQLITE_DONE) {
335 log_debug(
"Session deleted from database");
asciichat_error_t database_load_sessions(sqlite3 *db, session_registry_t *registry)
Load active sessions from database into registry.
void database_close(sqlite3 *db)
Close database.
asciichat_error_t database_save_session(sqlite3 *db, const session_entry_t *session)
Save session to database.
asciichat_error_t database_init(const char *db_path, sqlite3 **db)
Initialize database and create schema.
asciichat_error_t database_delete_session(sqlite3 *db, const uint8_t session_id[16])
Delete session from database.
💾 SQLite persistence for discovery service
#define SAFE_STRNCPY(dst, src, size)
#define SAFE_MALLOC(size, cast)
unsigned long long uint64_t
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
asciichat_error_t
Error and exit codes - unified status values (0-255)
#define log_warn(...)
Log a WARN message.
#define log_error(...)
Log an ERROR message.
#define log_info(...)
Log an INFO message.
#define log_debug(...)
Log a DEBUG message.
📝 Logging API with multiple log levels and terminal output control
🎯 Session registry for discovery service (lock-free RCU implementation)
#define MAX_PARTICIPANTS
Maximum participants per session.
Participant in a session.
uint8_t participant_id[16]
UUID.
uint8_t identity_pubkey[32]
Ed25519 public key.
uint64_t joined_at
Unix timestamp (ms)
Session entry (RCU hash table node)
bool has_password
Password protection flag.
uint8_t max_participants
1-8
uint8_t host_pubkey[32]
Host's Ed25519 key.
struct cds_lfht_node hash_node
RCU lock-free hash table node (keyed by session_string)
uint8_t session_id[16]
UUID.
char password_hash[128]
Argon2id hash (if has_password)
uint64_t created_at
Unix timestamp (ms)
char session_string[48]
e.g., "swift-river-mountain" (lookup key)
uint64_t expires_at
Unix timestamp (ms) - created_at + 24h.
uint8_t capabilities
bit 0: video, bit 1: audio
participant_t * participants[8]
Participant array.
uint8_t current_participants
Active participant count.
Session registry (lock-free RCU)
struct cds_lfht * sessions
RCU lock-free hash table.
⏱️ High-precision timing utilities using sokol_time.h and uthash