ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
database.h File Reference

💾 SQLite persistence for discovery service More...

Go to the source code of this file.

Functions

asciichat_error_t database_init (const char *db_path, sqlite3 **db)
 Initialize database and create schema.
 
asciichat_error_t database_load_sessions (sqlite3 *db, session_registry_t *registry)
 Load active sessions from database into registry.
 
asciichat_error_t database_save_session (sqlite3 *db, const session_entry_t *session)
 Save session to database.
 
asciichat_error_t database_delete_session (sqlite3 *db, const uint8_t session_id[16])
 Delete session from database.
 
void database_close (sqlite3 *db)
 Close database.
 

Detailed Description

💾 SQLite persistence for discovery service

Handles session persistence, rate limiting, and recovery from crashes.

Definition in file database.h.

Function Documentation

◆ database_close()

void database_close ( sqlite3 *  db)

Close database.

Parameters
dbDatabase handle to close

Definition at line 339 of file database.c.

339 {
340 if (!db) {
341 return;
342 }
343
344 sqlite3_close(db);
345 log_debug("Database closed");
346}
#define log_debug(...)
Log a DEBUG message.

References log_debug.

Referenced by acds_server_init(), and acds_server_shutdown().

◆ database_delete_session()

asciichat_error_t database_delete_session ( sqlite3 *  db,
const uint8_t  session_id[16] 
)

Delete session from database.

Parameters
dbDatabase handle
session_idSession UUID
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 313 of file database.c.

313 {
314 if (!db || !session_id) {
315 return SET_ERRNO(ERROR_INVALID_PARAM, "db or session_id is NULL");
316 }
317
318 const char *sql = "DELETE FROM sessions WHERE session_id = ?";
319
320 sqlite3_stmt *stmt = NULL;
321 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
322 if (rc != SQLITE_OK) {
323 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session delete query: %s", sqlite3_errmsg(db));
324 }
325
326 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
327
328 rc = sqlite3_step(stmt);
329 sqlite3_finalize(stmt);
330
331 if (rc != SQLITE_DONE) {
332 return SET_ERRNO(ERROR_CONFIG, "Failed to delete session: %s", sqlite3_errmsg(db));
333 }
334
335 log_debug("Session deleted from database");
336 return ASCIICHAT_OK;
337}
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
@ ASCIICHAT_OK
Definition error_codes.h:48
@ ERROR_CONFIG
Definition error_codes.h:54
@ ERROR_INVALID_PARAM
uint8_t session_id[16]

References ASCIICHAT_OK, ERROR_CONFIG, ERROR_INVALID_PARAM, log_debug, session_id, and SET_ERRNO.

◆ database_init()

asciichat_error_t database_init ( const char *  db_path,
sqlite3 **  db 
)

Initialize database and create schema.

Parameters
db_pathPath to SQLite database file
dbOutput database handle
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 58 of file database.c.

58 {
59 if (!db_path || !db) {
60 return SET_ERRNO(ERROR_INVALID_PARAM, "db_path or db is NULL");
61 }
62
63 log_info("Opening database: %s", db_path);
64
65 // Open database
66 int rc = sqlite3_open(db_path, db);
67 if (rc != SQLITE_OK) {
68 const char *err = sqlite3_errmsg(*db);
69 sqlite3_close(*db);
70 *db = NULL;
71 return SET_ERRNO(ERROR_CONFIG, "Failed to open database: %s", err);
72 }
73
74 // Enable Write-Ahead Logging for better concurrency
75 char *err_msg = NULL;
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);
80 }
81
82 // Enable foreign key constraints
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);
87 sqlite3_close(*db);
88 *db = NULL;
89 return SET_ERRNO(ERROR_CONFIG, "Failed to enable foreign keys");
90 }
91
92 // Create schema
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);
97 sqlite3_close(*db);
98 *db = NULL;
99 return SET_ERRNO(ERROR_CONFIG, "Failed to create database schema");
100 }
101
102 log_info("Database initialized successfully");
103 return ASCIICHAT_OK;
104}
#define log_warn(...)
Log a WARN message.
#define log_error(...)
Log an ERROR message.
#define log_info(...)
Log an INFO message.

References ASCIICHAT_OK, ERROR_CONFIG, ERROR_INVALID_PARAM, log_error, log_info, log_warn, and SET_ERRNO.

Referenced by acds_server_init().

◆ database_load_sessions()

asciichat_error_t database_load_sessions ( sqlite3 *  db,
session_registry_t registry 
)

Load active sessions from database into registry.

Parameters
dbDatabase handle
registrySession registry to populate
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 106 of file database.c.

106 {
107 if (!db || !registry) {
108 return SET_ERRNO(ERROR_INVALID_PARAM, "db or registry is NULL");
109 }
110
111 // Get current time
112 struct timespec ts;
113 clock_gettime(CLOCK_REALTIME, &ts);
114 uint64_t now = (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
115
116 // Prepare statement to load non-expired sessions
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 > ?";
120
121 sqlite3_stmt *stmt = NULL;
122 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
123 if (rc != SQLITE_OK) {
124 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session load query: %s", sqlite3_errmsg(db));
125 }
126
127 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now);
128
129 size_t loaded_count = 0;
130
131 // Iterate through sessions
132 while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
133 // Allocate session entry
135 if (!session) {
136 sqlite3_finalize(stmt);
137 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate session entry");
138 }
139
140 memset(session, 0, sizeof(*session));
141
142 // Load session data
143 memcpy(session->session_id, sqlite3_column_blob(stmt, 0), 16);
144 const char *session_string = (const char *)sqlite3_column_text(stmt, 1);
145 SAFE_STRNCPY(session->session_string, session_string, sizeof(session->session_string));
146 memcpy(session->host_pubkey, sqlite3_column_blob(stmt, 2), 32);
147
148 const char *password_hash = (const char *)sqlite3_column_text(stmt, 3);
149 if (password_hash) {
150 SAFE_STRNCPY(session->password_hash, password_hash, sizeof(session->password_hash));
151 session->has_password = true;
152 } else {
153 session->has_password = false;
154 }
155
156 session->max_participants = (uint8_t)sqlite3_column_int(stmt, 4);
157 session->capabilities = (uint8_t)sqlite3_column_int(stmt, 5);
158 session->created_at = (uint64_t)sqlite3_column_int64(stmt, 6);
159 session->expires_at = (uint64_t)sqlite3_column_int64(stmt, 7);
160 session->current_participants = 0; // Will be updated when loading participants
161
162 // Load participants for this session
163 sqlite3_stmt *part_stmt = NULL;
164 const char *part_sql = "SELECT participant_id, identity_pubkey, joined_at "
165 "FROM participants WHERE session_id = ?";
166
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);
170
171 size_t part_idx = 0;
172 while ((rc = sqlite3_step(part_stmt)) == SQLITE_ROW && part_idx < MAX_PARTICIPANTS) {
173 participant_t *participant = SAFE_MALLOC(sizeof(participant_t), participant_t *);
174 if (participant) {
175 memcpy(participant->participant_id, sqlite3_column_blob(part_stmt, 0), 16);
176 memcpy(participant->identity_pubkey, sqlite3_column_blob(part_stmt, 1), 32);
177 participant->joined_at = (uint64_t)sqlite3_column_int64(part_stmt, 2);
178
179 session->participants[part_idx] = participant;
180 session->current_participants++;
181 part_idx++;
182 }
183 }
184
185 sqlite3_finalize(part_stmt);
186 }
187
188 /* Add to RCU hash table (no lock needed)
189 RCU provides automatic synchronization for concurrent readers */
190 unsigned long hash = 5381; // DJB2 hash
191 const char *str = session->session_string;
192 int c;
193 while ((c = (unsigned char)*str++)) {
194 hash = ((hash << 5) + hash) + c;
195 }
196
197 cds_lfht_node_init(&session->hash_node);
198 struct cds_lfht_node *ret_node = cds_lfht_add_unique(registry->sessions, hash,
199 /* match callback */
200 NULL, // Using NULL match for now (will use default equality)
201 session->session_string, &session->hash_node);
202
203 if (ret_node != &session->hash_node) {
204 /* Session already exists - free this duplicate */
205 for (size_t i = 0; i < MAX_PARTICIPANTS; i++) {
206 SAFE_FREE(session->participants[i]);
207 }
208 SAFE_FREE(session);
209 log_warn("Duplicate session in database: %s (skipping)", session_string);
210 continue;
211 }
212
213 loaded_count++;
214 }
215
216 sqlite3_finalize(stmt);
217
218 if (rc != SQLITE_DONE) {
219 log_warn("Session loading ended with status %d: %s", rc, sqlite3_errmsg(db));
220 }
221
222 log_info("Loaded %zu sessions from database", loaded_count);
223 return ASCIICHAT_OK;
224}
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MALLOC(size, cast)
Definition common.h:208
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
@ ERROR_MEMORY
Definition error_codes.h:53
#define MAX_PARTICIPANTS
Maximum participants per session.
Definition session.h:36
Participant in a session.
Definition session.h:41
uint8_t participant_id[16]
UUID.
Definition session.h:42
uint8_t identity_pubkey[32]
Ed25519 public key.
Definition session.h:43
uint64_t joined_at
Unix timestamp (ms)
Definition session.h:44
Session entry (RCU hash table node)
Definition session.h:55
bool has_password
Password protection flag.
Definition session.h:65
uint8_t max_participants
1-8
Definition session.h:61
uint8_t host_pubkey[32]
Host's Ed25519 key.
Definition session.h:59
struct cds_lfht_node hash_node
RCU lock-free hash table node (keyed by session_string)
Definition session.h:80
uint8_t session_id[16]
UUID.
Definition session.h:57
char password_hash[128]
Argon2id hash (if has_password)
Definition session.h:64
uint64_t created_at
Unix timestamp (ms)
Definition session.h:69
char session_string[48]
e.g., "swift-river-mountain" (lookup key)
Definition session.h:56
uint64_t expires_at
Unix timestamp (ms) - created_at + 24h.
Definition session.h:70
uint8_t capabilities
bit 0: video, bit 1: audio
Definition session.h:60
participant_t * participants[8]
Participant array.
Definition session.h:76
uint8_t current_participants
Active participant count.
Definition session.h:62
struct cds_lfht * sessions
RCU lock-free hash table.
Definition session.h:93

References ASCIICHAT_OK, session_entry::capabilities, session_entry::created_at, session_entry::current_participants, ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_MEMORY, session_entry::expires_at, session_entry::has_password, session_entry::hash_node, session_entry::host_pubkey, participant_t::identity_pubkey, participant_t::joined_at, log_info, log_warn, MAX_PARTICIPANTS, session_entry::max_participants, participant_t::participant_id, session_entry::participants, session_entry::password_hash, SAFE_FREE, SAFE_MALLOC, SAFE_STRNCPY, session_entry::session_id, session_entry::session_string, session_registry_t::sessions, and SET_ERRNO.

Referenced by acds_server_init().

◆ database_save_session()

asciichat_error_t database_save_session ( sqlite3 *  db,
const session_entry_t session 
)

Save session to database.

Parameters
dbDatabase handle
sessionSession entry to save
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 226 of file database.c.

226 {
227 if (!db || !session) {
228 return SET_ERRNO(ERROR_INVALID_PARAM, "db or session is NULL");
229 }
230
231 // Begin transaction
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);
237 return SET_ERRNO(ERROR_CONFIG, "Failed to begin transaction");
238 }
239
240 // Insert or replace session
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 (?, ?, ?, ?, ?, ?, ?, ?)";
245
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);
250 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session save query: %s", sqlite3_errmsg(db));
251 }
252
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);
256 sqlite3_bind_text(stmt, 4, session->has_password ? session->password_hash : NULL, -1, SQLITE_STATIC);
257 sqlite3_bind_int(stmt, 5, session->max_participants);
258 sqlite3_bind_int(stmt, 6, session->capabilities);
259 sqlite3_bind_int64(stmt, 7, (sqlite3_int64)session->created_at);
260 sqlite3_bind_int64(stmt, 8, (sqlite3_int64)session->expires_at);
261
262 rc = sqlite3_step(stmt);
263 sqlite3_finalize(stmt);
264
265 if (rc != SQLITE_DONE) {
266 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
267 return SET_ERRNO(ERROR_CONFIG, "Failed to save session: %s", sqlite3_errmsg(db));
268 }
269
270 // Delete old participants (will re-insert current ones)
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);
275 sqlite3_step(stmt);
276 sqlite3_finalize(stmt);
277 }
278
279 // Insert participants
280 const char *part_sql = "INSERT INTO participants (participant_id, session_id, identity_pubkey, joined_at) "
281 "VALUES (?, ?, ?, ?)";
282
283 for (size_t i = 0; i < MAX_PARTICIPANTS; i++) {
284 if (session->participants[i]) {
285 rc = sqlite3_prepare_v2(db, part_sql, -1, &stmt, NULL);
286 if (rc != SQLITE_OK) {
287 continue;
288 }
289
290 sqlite3_bind_blob(stmt, 1, session->participants[i]->participant_id, 16, SQLITE_STATIC);
291 sqlite3_bind_blob(stmt, 2, session->session_id, 16, SQLITE_STATIC);
292 sqlite3_bind_blob(stmt, 3, session->participants[i]->identity_pubkey, 32, SQLITE_STATIC);
293 sqlite3_bind_int64(stmt, 4, (sqlite3_int64)session->participants[i]->joined_at);
294
295 sqlite3_step(stmt);
296 sqlite3_finalize(stmt);
297 }
298 }
299
300 // Commit transaction
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);
306 return SET_ERRNO(ERROR_CONFIG, "Failed to commit transaction");
307 }
308
309 log_debug("Session %s saved to database", session->session_string);
310 return ASCIICHAT_OK;
311}

References ASCIICHAT_OK, session_entry::capabilities, session_entry::created_at, ERROR_CONFIG, ERROR_INVALID_PARAM, session_entry::expires_at, session_entry::has_password, session_entry::host_pubkey, participant_t::identity_pubkey, participant_t::joined_at, log_debug, log_error, MAX_PARTICIPANTS, session_entry::max_participants, participant_t::participant_id, session_entry::participants, session_entry::password_hash, session_entry::session_id, session_entry::session_string, and SET_ERRNO.