ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
database.h File Reference

Go to the source code of this file.

Typedefs

typedef sqlite3 discovery_database_t
 Discovery database handle (sqlite3 wrapper)
 

Functions

asciichat_error_t database_init (const char *db_path, sqlite3 **db)
 Initialize database and create schema.
 
void database_close (sqlite3 *db)
 Close database.
 
asciichat_error_t database_session_create (sqlite3 *db, const acip_session_create_t *req, const acds_config_t *config, acip_session_created_t *resp)
 Create new session.
 
asciichat_error_t database_session_lookup (sqlite3 *db, const char *session_string, const acds_config_t *config, acip_session_info_t *resp)
 Lookup session by string.
 
asciichat_error_t database_session_join (sqlite3 *db, const acip_session_join_t *req, const acds_config_t *config, acip_session_joined_t *resp)
 Join existing session.
 
asciichat_error_t database_session_leave (sqlite3 *db, const uint8_t session_id[16], const uint8_t participant_id[16])
 Leave session.
 
session_entry_t * database_session_find_by_id (sqlite3 *db, const uint8_t session_id[16])
 Find session by session_id.
 
session_entry_t * database_session_find_by_string (sqlite3 *db, const char *session_string)
 Find session by session_string.
 
void database_session_cleanup_expired (sqlite3 *db)
 Clean up inactive sessions.
 
asciichat_error_t database_session_update_host (sqlite3 *db, const uint8_t session_id[16], const uint8_t host_participant_id[16], const char *host_address, uint16_t host_port, uint8_t connection_type)
 Update session host (discovery mode)
 
asciichat_error_t database_session_clear_host (sqlite3 *db, const uint8_t session_id[16])
 Clear session host (discovery mode - host migration)
 
asciichat_error_t database_session_start_migration (sqlite3 *db, const uint8_t session_id[16])
 Start host migration (discovery mode)
 
bool database_session_is_migration_ready (sqlite3 *db, const uint8_t session_id[16], uint64_t migration_window_ms)
 Check if migration window has completed.
 
asciichat_error_t database_session_add_key (sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32], uint32_t key_version)
 Add a key to a session.
 
asciichat_error_t database_session_revoke_key (sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32])
 Revoke a key from a session.
 
bool database_session_verify_key (sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32])
 Check if a key is valid for a session (exists and not revoked)
 
asciichat_error_t database_session_get_keys (sqlite3 *db, const char *session_string, uint8_t(*keys_out)[32], size_t max_keys, size_t *count_out)
 Get all active keys for a session.
 
asciichat_error_t database_stats_session_count (sqlite3 *db, int64_t *count_out)
 Get total number of active sessions.
 
asciichat_error_t database_stats_unique_clients (sqlite3 *db, int64_t *count_out)
 Get total number of unique clients/participants.
 

Typedef Documentation

◆ discovery_database_t

typedef sqlite3 discovery_database_t

Discovery database handle (sqlite3 wrapper)

Definition at line 24 of file database.h.

Function Documentation

◆ database_close()

void database_close ( sqlite3 *  db)

Close database.

Parameters
dbDatabase handle to close

Definition at line 310 of file discovery/database.c.

310 {
311 if (!db) {
312 return;
313 }
314 sqlite3_close(db);
315 log_debug("Database closed");
316}
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548

References log_debug.

Referenced by acds_server_init(), and acds_server_shutdown().

◆ 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 263 of file discovery/database.c.

263 {
264 if (!db_path || !db) {
265 return SET_ERRNO(ERROR_INVALID_PARAM, "db_path or db is NULL");
266 }
267
268 log_info("Opening database: %s", db_path);
269
270 int rc = sqlite3_open(db_path, db);
271 if (rc != SQLITE_OK) {
272 const char *err = sqlite3_errmsg(*db);
273 sqlite3_close(*db);
274 *db = NULL;
275 return SET_ERRNO(ERROR_CONFIG, "Failed to open database: %s", err);
276 }
277
278 // Enable Write-Ahead Logging for concurrent reads
279 char *err_msg = NULL;
280 rc = sqlite3_exec(*db, "PRAGMA journal_mode=WAL;", NULL, NULL, &err_msg);
281 if (rc != SQLITE_OK) {
282 log_warn("Failed to enable WAL mode: %s", err_msg ? err_msg : "unknown error");
283 sqlite3_free(err_msg);
284 }
285
286 // Enable foreign key constraints
287 rc = sqlite3_exec(*db, "PRAGMA foreign_keys=ON;", NULL, NULL, &err_msg);
288 if (rc != SQLITE_OK) {
289 log_error("Failed to enable foreign keys: %s", err_msg ? err_msg : "unknown error");
290 sqlite3_free(err_msg);
291 sqlite3_close(*db);
292 *db = NULL;
293 return SET_ERRNO(ERROR_CONFIG, "Failed to enable foreign keys");
294 }
295
296 // Create schema
297 rc = sqlite3_exec(*db, schema_sql, NULL, NULL, &err_msg);
298 if (rc != SQLITE_OK) {
299 log_error("Failed to create schema: %s", err_msg ? err_msg : "unknown error");
300 sqlite3_free(err_msg);
301 sqlite3_close(*db);
302 *db = NULL;
303 return SET_ERRNO(ERROR_CONFIG, "Failed to create database schema");
304 }
305
306 log_info("Database initialized successfully (SQLite as single source of truth)");
307 return ASCIICHAT_OK;
308}
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_CONFIG
Definition error_codes.h:57
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561

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

Referenced by acds_server_init().

◆ database_session_add_key()

asciichat_error_t database_session_add_key ( sqlite3 *  db,
const char *  session_string,
const uint8_t  identity_pubkey[32],
uint32_t  key_version 
)

Add a key to a session.

Parameters
dbDatabase handle
session_stringSession string
identity_pubkeyEd25519 public key (32 bytes)
key_versionKey version for tracking rotation
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 1050 of file discovery/database.c.

1051 {
1052 if (!db || !session_string || !identity_pubkey) {
1053 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for add_key");
1054 }
1055
1056 const char *sql = "INSERT OR IGNORE INTO session_keys (session_string, identity_pubkey, key_version, added_at) "
1057 "VALUES (?, ?, ?, ?)";
1058
1059 sqlite3_stmt *stmt = NULL;
1060 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1061 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare add_key statement: %s", sqlite3_errmsg(db));
1062 }
1063
1064 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1065 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1066 sqlite3_bind_int(stmt, 3, key_version);
1067 sqlite3_bind_int64(stmt, 4, database_get_current_time_ms());
1068
1069 int rc = sqlite3_step(stmt);
1070 sqlite3_finalize(stmt);
1071
1072 if (rc != SQLITE_DONE) {
1073 return SET_ERRNO(ERROR_CONFIG, "Failed to add session key: %s", sqlite3_errmsg(db));
1074 }
1075
1076 log_debug("Added key to session %s (version=%u)", session_string, key_version);
1077 return ASCIICHAT_OK;
1078}

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

◆ database_session_cleanup_expired()

void database_session_cleanup_expired ( sqlite3 *  db)

Clean up inactive sessions.

Removes sessions that have been inactive for more than 3 hours. Inactivity is measured by last_activity_at (updated on join/leave/host updates). Called periodically by background cleanup thread.

Parameters
dbDatabase handle

Definition at line 845 of file discovery/database.c.

845 {
846 if (!db) {
847 return;
848 }
849
850 uint64_t now = database_get_current_time_ms();
851 // 3 hours in milliseconds
852 uint64_t inactivity_threshold = 3ULL * SEC_PER_HOUR * MS_PER_SEC_INT;
853 uint64_t cutoff_time = now - inactivity_threshold;
854
855 // Log sessions about to be deleted
856 const char *log_sql = "SELECT session_string, last_activity_at FROM sessions WHERE last_activity_at < ?";
857 sqlite3_stmt *stmt = NULL;
858 if (sqlite3_prepare_v2(db, log_sql, -1, &stmt, NULL) == SQLITE_OK) {
859 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff_time);
860 while (sqlite3_step(stmt) == SQLITE_ROW) {
861 const char *session_string = (const char *)sqlite3_column_text(stmt, 0);
862 uint64_t last_activity = (uint64_t)sqlite3_column_int64(stmt, 1);
863 uint64_t inactive_ms = now - last_activity;
864 uint64_t inactive_hours = inactive_ms / (SEC_PER_HOUR * MS_PER_SEC_INT);
865 log_info("Session %s inactive for %lu hours, deleting", session_string ? session_string : "<unknown>",
866 inactive_hours);
867 }
868 sqlite3_finalize(stmt);
869 }
870
871 // Delete inactive sessions (CASCADE deletes participants)
872 const char *del_sql = "DELETE FROM sessions WHERE last_activity_at < ?";
873 if (sqlite3_prepare_v2(db, del_sql, -1, &stmt, NULL) == SQLITE_OK) {
874 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff_time);
875 sqlite3_step(stmt);
876 int deleted = sqlite3_changes(db);
877 sqlite3_finalize(stmt);
878
879 if (deleted > 0) {
880 log_info("Cleaned up %d inactive sessions (>3 hours)", deleted);
881 }
882 }
883}
unsigned long long uint64_t
Definition common.h:59
#define SEC_PER_HOUR
Definition time.h:168
#define MS_PER_SEC_INT
Definition time.h:164

References log_info, MS_PER_SEC_INT, and SEC_PER_HOUR.

◆ database_session_clear_host()

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

Clear session host (discovery mode - host migration)

Called when the current host disconnects or fails. Clears host fields so remaining participants can negotiate a new host.

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

Definition at line 934 of file discovery/database.c.

934 {
935 if (!db || !session_id) {
936 return SET_ERRNO(ERROR_INVALID_PARAM, "db or session_id is NULL");
937 }
938
939 const char *sql = "UPDATE sessions SET "
940 "host_established = 0, "
941 "host_participant_id = NULL, "
942 "host_address = NULL, "
943 "host_port = 0, "
944 "host_connection_type = 0 "
945 "WHERE session_id = ?";
946
947 sqlite3_stmt *stmt = NULL;
948 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
949 if (rc != SQLITE_OK) {
950 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare host clear: %s", sqlite3_errmsg(db));
951 }
952
953 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
954
955 rc = sqlite3_step(stmt);
956 sqlite3_finalize(stmt);
957
958 if (rc != SQLITE_DONE) {
959 return SET_ERRNO(ERROR_CONFIG, "Failed to clear host: %s", sqlite3_errmsg(db));
960 }
961
962 int changes = sqlite3_changes(db);
963 if (changes == 0) {
964 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for host clear");
965 }
966
967 log_info("Session host cleared (session=%02x%02x...) - ready for migration", session_id[0], session_id[1]);
968
969 return ASCIICHAT_OK;
970}
@ ERROR_INVALID_STATE
uint8_t session_id[16]

References ASCIICHAT_OK, ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, log_info, session_id, and SET_ERRNO.

◆ database_session_create()

asciichat_error_t database_session_create ( sqlite3 *  db,
const acip_session_create_t *  req,
const acds_config_t *  config,
acip_session_created_t *  resp 
)

Create new session.

Parameters
dbDatabase handle
reqSession creation request
configACDS server configuration
respSession creation response (output)
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 322 of file discovery/database.c.

323 {
324 if (!db || !req || !config || !resp) {
325 return SET_ERRNO(ERROR_INVALID_PARAM, "db, req, config, or resp is NULL");
326 }
327
328 memset(resp, 0, sizeof(*resp));
329
330 // Generate or use reserved session string
331 char session_string[ACIP_MAX_SESSION_STRING_LEN] = {0};
332 if (req->reserved_string_len > 0) {
333 const char *reserved_str = (const char *)(req + 1);
334 size_t len = req->reserved_string_len < (ACIP_MAX_SESSION_STRING_LEN - 1) ? req->reserved_string_len
336 memcpy(session_string, reserved_str, len);
337 session_string[len] = '\0';
338
339 if (!is_session_string(session_string)) {
341 if (HAS_ERRNO(&ctx)) {
342 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session string: %s (%s)", session_string, ctx.context_message);
343 }
344 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session string: %s", session_string);
345 }
346 } else {
347 asciichat_error_t result = acds_string_generate(session_string, sizeof(session_string));
348 if (result != ASCIICHAT_OK) {
349 return result;
350 }
351 }
352
353 // Generate session ID
355 generate_uuid(session_id);
356
357 // Set timestamps
358 uint64_t now = database_get_current_time_ms();
359 uint64_t expires_at = now + ACIP_SESSION_EXPIRATION_MS;
360
361 // Calculate max_participants
362 uint8_t max_participants =
363 req->max_participants > 0 && req->max_participants <= MAX_PARTICIPANTS ? req->max_participants : MAX_PARTICIPANTS;
364
365 // Insert into database
366 const char *sql = "INSERT INTO sessions "
367 "(session_string, session_id, host_pubkey, password_hash, max_participants, "
368 "current_participants, capabilities, has_password, expose_ip_publicly, "
369 "session_type, server_address, server_port, created_at, expires_at, last_activity_at) "
370 "VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
371
372 sqlite3_stmt *stmt = NULL;
373 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
374 if (rc != SQLITE_OK) {
375 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session insert: %s", sqlite3_errmsg(db));
376 }
377
378 log_info("DATABASE_SESSION_CREATE: expose_ip_publicly=%d, server_address='%s' server_port=%u, session_type=%u, "
379 "has_password=%u",
380 req->expose_ip_publicly, req->server_address, req->server_port, req->session_type, req->has_password);
381 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
382 sqlite3_bind_blob(stmt, 2, session_id, 16, SQLITE_STATIC);
383 sqlite3_bind_blob(stmt, 3, req->identity_pubkey, 32, SQLITE_STATIC);
384 sqlite3_bind_text(stmt, 4, req->has_password ? (const char *)req->password_hash : NULL, -1, SQLITE_STATIC);
385 sqlite3_bind_int(stmt, 5, max_participants);
386 sqlite3_bind_int(stmt, 6, req->capabilities);
387 sqlite3_bind_int(stmt, 7, req->has_password ? 1 : 0);
388 sqlite3_bind_int(stmt, 8, req->expose_ip_publicly ? 1 : 0);
389 sqlite3_bind_int(stmt, 9, req->session_type);
390 sqlite3_bind_text(stmt, 10, req->server_address, -1, SQLITE_STATIC);
391 sqlite3_bind_int(stmt, 11, req->server_port);
392 sqlite3_bind_int64(stmt, 12, (sqlite3_int64)now);
393 sqlite3_bind_int64(stmt, 13, (sqlite3_int64)expires_at);
394 sqlite3_bind_int64(stmt, 14, (sqlite3_int64)now); // last_activity_at = created_at
395
396 rc = sqlite3_step(stmt);
397 sqlite3_finalize(stmt);
398
399 if (rc != SQLITE_DONE) {
400 if (rc == SQLITE_CONSTRAINT) {
401 return SET_ERRNO(ERROR_INVALID_STATE, "Session string already exists: %s", session_string);
402 }
403 return SET_ERRNO(ERROR_CONFIG, "Failed to insert session: %s", sqlite3_errmsg(db));
404 }
405
406 // Fill response
407 resp->session_string_len = (uint8_t)strlen(session_string);
408 SAFE_STRNCPY(resp->session_string, session_string, sizeof(resp->session_string));
409 memcpy(resp->session_id, session_id, 16);
410 resp->expires_at = expires_at;
411 resp->stun_count = config->stun_count;
412 resp->turn_count = config->turn_count;
413
414 log_info("Session created: %s (session_id=%02x%02x%02x%02x..., max_participants=%d, has_password=%d)", session_string,
415 session_id[0], session_id[1], session_id[2], session_id[3], max_participants, req->has_password ? 1 : 0);
416
417 return ASCIICHAT_OK;
418}
bool is_session_string(const char *str)
Check if a string is a valid session string.
asciichat_error_t acds_string_generate(char *output, size_t output_size)
Generate random session string.
#define ACIP_MAX_SESSION_STRING_LEN
Maximum session string length (e.g., "swift-river-mountain" = 20 chars)
Definition acds.h:1085
#define ACIP_SESSION_EXPIRATION_MS
Session maximum lifetime (24 hours in milliseconds) Note: Sessions are actually cleaned up after 3 ho...
Definition acds.h:1091
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
unsigned char uint8_t
Definition common.h:56
#define HAS_ERRNO(var)
Check if an error occurred and get full context.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
#define MAX_PARTICIPANTS
uint8_t turn_count
Number of configured TURN servers (0-4)
uint8_t stun_count
Number of configured STUN servers (0-4)
Error context structure.
char * context_message
Optional custom message (dynamically allocated, owned by system)

References acds_string_generate(), ACIP_MAX_SESSION_STRING_LEN, ACIP_SESSION_EXPIRATION_MS, ASCIICHAT_OK, asciichat_error_context_t::context_message, ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, HAS_ERRNO, is_session_string(), log_info, MAX_PARTICIPANTS, SAFE_STRNCPY, session_id, SET_ERRNO, acds_config_t::stun_count, and acds_config_t::turn_count.

◆ database_session_find_by_id()

session_entry_t * database_session_find_by_id ( sqlite3 *  db,
const uint8_t  session_id[16] 
)

Find session by session_id.

Allocates and returns a session_entry_t. Caller must free with session_entry_destroy().

Parameters
dbDatabase handle
session_idSession UUID to find
Returns
Allocated session entry (caller owns), or NULL if not found

Definition at line 793 of file discovery/database.c.

793 {
794 if (!db || !session_id) {
795 return NULL;
796 }
797
798 static const char sql[] = SELECT_SESSION_BASE " WHERE session_id = ?";
799
800 sqlite3_stmt *stmt = NULL;
801 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
802 return NULL;
803 }
804
805 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
806
807 session_entry_t *session = NULL;
808 if (sqlite3_step(stmt) == SQLITE_ROW) {
809 session = load_session_from_row(stmt);
810 if (session) {
811 load_session_participants(db, session);
812 }
813 }
814
815 sqlite3_finalize(stmt);
816 return session;
817}
#define SELECT_SESSION_BASE
Session entry data structure.

References SELECT_SESSION_BASE, and session_id.

Referenced by signaling_broadcast(), signaling_relay_ice(), signaling_relay_network_quality(), and signaling_relay_sdp().

◆ database_session_find_by_string()

session_entry_t * database_session_find_by_string ( sqlite3 *  db,
const char *  session_string 
)

Find session by session_string.

Allocates and returns a session_entry_t. Caller must free with session_entry_destroy().

Parameters
dbDatabase handle
session_stringSession string to find
Returns
Allocated session entry (caller owns), or NULL if not found

Definition at line 819 of file discovery/database.c.

819 {
820 if (!db || !session_string) {
821 return NULL;
822 }
823
824 static const char sql[] = SELECT_SESSION_BASE " WHERE session_string = ?";
825
826 sqlite3_stmt *stmt = NULL;
827 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
828 return NULL;
829 }
830
831 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
832
833 session_entry_t *session = NULL;
834 if (sqlite3_step(stmt) == SQLITE_ROW) {
835 session = load_session_from_row(stmt);
836 if (session) {
837 load_session_participants(db, session);
838 }
839 }
840
841 sqlite3_finalize(stmt);
842 return session;
843}

References SELECT_SESSION_BASE.

Referenced by database_session_join().

◆ database_session_get_keys()

asciichat_error_t database_session_get_keys ( sqlite3 *  db,
const char *  session_string,
uint8_t(*)  keys_out[32],
size_t  max_keys,
size_t *  count_out 
)

Get all active keys for a session.

Parameters
dbDatabase handle
session_stringSession string
keys_outOutput array for keys (caller must provide buffer)
max_keysMaximum number of keys to retrieve
count_outOutput: number of keys retrieved
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 1128 of file discovery/database.c.

1129 {
1130 if (!db || !session_string || !keys_out || !count_out) {
1131 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for get_keys");
1132 }
1133
1134 const char *sql = "SELECT identity_pubkey FROM session_keys WHERE session_string = ? AND revoked = 0 "
1135 "ORDER BY key_version ASC";
1136
1137 sqlite3_stmt *stmt = NULL;
1138 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1139 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare get_keys statement: %s", sqlite3_errmsg(db));
1140 }
1141
1142 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1143
1144 size_t count = 0;
1145 while (sqlite3_step(stmt) == SQLITE_ROW && count < max_keys) {
1146 const void *blob = sqlite3_column_blob(stmt, 0);
1147 if (blob && sqlite3_column_bytes(stmt, 0) == 32) {
1148 memcpy(keys_out[count], blob, 32);
1149 count++;
1150 }
1151 }
1152
1153 sqlite3_finalize(stmt);
1154 *count_out = count;
1155
1156 log_debug("Retrieved %zu keys for session %s", count, session_string);
1157 return ASCIICHAT_OK;
1158}

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

◆ database_session_is_migration_ready()

bool database_session_is_migration_ready ( sqlite3 *  db,
const uint8_t  session_id[16],
uint64_t  migration_window_ms 
)

Check if migration window has completed.

Called periodically to check if migration collection window timeout has expired. Returns the session if migration is ready for host election.

Parameters
dbDatabase handle
session_idSession UUID
migration_window_msCollection window duration in milliseconds
Returns
true if collection window has completed, false otherwise

Definition at line 1011 of file discovery/database.c.

1011 {
1012 if (!db || !session_id) {
1013 return false;
1014 }
1015
1016 uint64_t now_ns = time_get_realtime_ns();
1017 const char *sql = "SELECT in_migration, migration_start_ns FROM sessions WHERE session_id = ?";
1018
1019 sqlite3_stmt *stmt = NULL;
1020 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1021 return false;
1022 }
1023
1024 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
1025
1026 bool ready = false;
1027 if (sqlite3_step(stmt) == SQLITE_ROW) {
1028 int in_migration = sqlite3_column_int(stmt, 0);
1029 uint64_t migration_start_ns = (uint64_t)sqlite3_column_int64(stmt, 1);
1030
1031 if (in_migration) {
1032 uint64_t elapsed_ns = now_ns - migration_start_ns;
1033 uint64_t migration_window_ns = migration_window_ms * NS_PER_MS_INT;
1034 if (elapsed_ns >= migration_window_ns) {
1035 ready = true;
1036 log_debug("Migration window complete (session=%02x%02x..., elapsed=%lu ns, window=%lu ns)", session_id[0],
1037 session_id[1], elapsed_ns, migration_window_ns);
1038 }
1039 }
1040 }
1041
1042 sqlite3_finalize(stmt);
1043 return ready;
1044}
uint64_t time_get_realtime_ns(void)
Get current wall-clock (real) time in nanoseconds.
Definition util/time.c:119
#define NS_PER_MS_INT
Definition time.h:156

References log_debug, NS_PER_MS_INT, session_id, and time_get_realtime_ns().

◆ database_session_join()

asciichat_error_t database_session_join ( sqlite3 *  db,
const acip_session_join_t *  req,
const acds_config_t *  config,
acip_session_joined_t *  resp 
)

Join existing session.

Parameters
dbDatabase handle
reqJoin request
configACDS server configuration (for TURN credentials)
respJoin response (output)
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 482 of file discovery/database.c.

483 {
484 if (!db || !req || !config || !resp) {
485 return SET_ERRNO(ERROR_INVALID_PARAM, "db, req, config, or resp is NULL");
486 }
487
488 memset(resp, 0, sizeof(*resp));
489 resp->success = 0;
490
491 // Extract session string
492 char session_string[ACIP_MAX_SESSION_STRING_LEN] = {0};
493 size_t len = req->session_string_len < (ACIP_MAX_SESSION_STRING_LEN - 1) ? req->session_string_len
495 memcpy(session_string, req->session_string, len);
496 session_string[len] = '\0';
497
498 // Find session
499 session_entry_t *session = database_session_find_by_string(db, session_string);
500 if (!session) {
501 resp->error_code = ACIP_ERROR_SESSION_NOT_FOUND;
502 SAFE_STRNCPY(resp->error_message, "Session not found", sizeof(resp->error_message));
503 log_warn("Session join failed: %s (not found)", session_string);
504 return ASCIICHAT_OK;
505 }
506
507 // Check if session is full
508 if (session->current_participants >= session->max_participants) {
509 session_entry_destroy(session);
510 resp->error_code = ACIP_ERROR_SESSION_FULL;
511 SAFE_STRNCPY(resp->error_message, "Session is full", sizeof(resp->error_message));
512 log_warn("Session join failed: %s (full)", session_string);
513 return ASCIICHAT_OK;
514 }
515
516 // Verify password if required
517 if (session->has_password && req->has_password) {
518 if (!verify_password(req->password, session->password_hash)) {
519 session_entry_destroy(session);
520 resp->error_code = ACIP_ERROR_INVALID_PASSWORD;
521 SAFE_STRNCPY(resp->error_message, "Invalid password", sizeof(resp->error_message));
522 log_warn("Session join failed: %s (invalid password)", session_string);
523 return ASCIICHAT_OK;
524 }
525 } else if (session->has_password && !req->has_password) {
526 session_entry_destroy(session);
527 resp->error_code = ACIP_ERROR_INVALID_PASSWORD;
528 SAFE_STRNCPY(resp->error_message, "Password required", sizeof(resp->error_message));
529 log_warn("Session join failed: %s (password required)", session_string);
530 return ASCIICHAT_OK;
531 }
532
533 // Generate participant ID
535 generate_uuid(participant_id);
536 uint64_t now = database_get_current_time_ms();
537
538 // Begin transaction
539 char *err_msg = NULL;
540 int rc = sqlite3_exec(db, "BEGIN IMMEDIATE;", NULL, NULL, &err_msg);
541 if (rc != SQLITE_OK) {
542 session_entry_destroy(session);
543 log_error("Failed to begin transaction: %s", err_msg ? err_msg : "unknown");
544 sqlite3_free(err_msg);
545 return SET_ERRNO(ERROR_CONFIG, "Failed to begin transaction");
546 }
547
548 // Insert participant
549 const char *insert_sql = "INSERT INTO participants (participant_id, session_string, identity_pubkey, joined_at) "
550 "VALUES (?, ?, ?, ?)";
551 sqlite3_stmt *stmt = NULL;
552 rc = sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL);
553 if (rc != SQLITE_OK) {
554 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
555 session_entry_destroy(session);
556 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare participant insert: %s", sqlite3_errmsg(db));
557 }
558
559 sqlite3_bind_blob(stmt, 1, participant_id, 16, SQLITE_STATIC);
560 sqlite3_bind_text(stmt, 2, session->session_string, -1, SQLITE_STATIC);
561 sqlite3_bind_blob(stmt, 3, req->identity_pubkey, 32, SQLITE_STATIC);
562 sqlite3_bind_int64(stmt, 4, (sqlite3_int64)now);
563
564 rc = sqlite3_step(stmt);
565 sqlite3_finalize(stmt);
566
567 if (rc != SQLITE_DONE) {
568 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
569 session_entry_destroy(session);
570 return SET_ERRNO(ERROR_CONFIG, "Failed to insert participant: %s", sqlite3_errmsg(db));
571 }
572
573 // Update participant count and last activity
574 const char *update_sql = "UPDATE sessions SET current_participants = current_participants + 1, "
575 "last_activity_at = ? WHERE session_string = ?";
576 rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL);
577 if (rc == SQLITE_OK) {
578 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now);
579 sqlite3_bind_text(stmt, 2, session->session_string, -1, SQLITE_STATIC);
580 sqlite3_step(stmt);
581 sqlite3_finalize(stmt);
582 }
583
584 // Set initiator_id if this is the first participant (for discovery mode tiebreaker)
585 bool is_first_participant = (session->current_participants == 0);
586 bool is_zero_initiator = true;
587 for (int i = 0; i < 16; i++) {
588 if (session->initiator_id[i] != 0) {
589 is_zero_initiator = false;
590 break;
591 }
592 }
593
594 if (is_first_participant || is_zero_initiator) {
595 const char *initiator_sql = "UPDATE sessions SET initiator_id = ? WHERE session_string = ?";
596 rc = sqlite3_prepare_v2(db, initiator_sql, -1, &stmt, NULL);
597 if (rc == SQLITE_OK) {
598 sqlite3_bind_blob(stmt, 1, participant_id, 16, SQLITE_STATIC);
599 sqlite3_bind_text(stmt, 2, session->session_string, -1, SQLITE_STATIC);
600 sqlite3_step(stmt);
601 sqlite3_finalize(stmt);
602 memcpy(session->initiator_id, participant_id, 16);
603 log_debug("Set initiator_id to new participant %02x%02x...", participant_id[0], participant_id[1]);
604 }
605 }
606
607 // Commit transaction
608 rc = sqlite3_exec(db, "COMMIT;", NULL, NULL, &err_msg);
609 if (rc != SQLITE_OK) {
610 log_error("Failed to commit transaction: %s", err_msg ? err_msg : "unknown");
611 sqlite3_free(err_msg);
612 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
613 session_entry_destroy(session);
614 return SET_ERRNO(ERROR_CONFIG, "Failed to commit transaction");
615 }
616
617 // Fill response
618 resp->success = 1;
619 resp->error_code = ACIP_ERROR_NONE;
620 memcpy(resp->participant_id, participant_id, 16);
621 memcpy(resp->session_id, session->session_id, 16);
622
623 // Discovery mode host negotiation fields
624 memcpy(resp->initiator_id, session->initiator_id, 16);
625 resp->host_established = session->host_established ? 1 : 0;
626 if (session->host_established) {
627 memcpy(resp->host_id, session->host_participant_id, 16);
628 }
629 // peer_count = current_participants (excluding self) that need to negotiate
630 resp->peer_count = session->current_participants; // Will be incremented after this returns
631 // The transport type is not private server-address data. WebRTC clients need
632 // it to choose the signaling path even while the server IP remains hidden.
633 resp->session_type = session->session_type;
634
635 log_debug("SESSION_JOIN response: session_id=%02x%02x%02x%02x..., participant_id=%02x%02x%02x%02x..., "
636 "initiator=%02x%02x..., host_established=%d, peer_count=%d",
637 resp->session_id[0], resp->session_id[1], resp->session_id[2], resp->session_id[3], resp->participant_id[0],
638 resp->participant_id[1], resp->participant_id[2], resp->participant_id[3], resp->initiator_id[0],
639 resp->initiator_id[1], resp->host_established, resp->peer_count);
640
641 // IP disclosure logic
642 log_info("DATABASE_SESSION_JOIN: has_password=%d, expose_ip_publicly=%d, server_address='%s'", session->has_password,
643 session->expose_ip_publicly, session->server_address);
644 bool reveal_ip = session->has_password || session->expose_ip_publicly;
645 log_info("DATABASE_SESSION_JOIN: reveal_ip=%d", reveal_ip);
646
647 if (reveal_ip) {
648 SAFE_STRNCPY(resp->server_address, session->server_address, sizeof(resp->server_address));
649 resp->server_port = session->server_port;
650 // Generate TURN credentials for WebRTC sessions
651 if (session->session_type == SESSION_TYPE_WEBRTC && config->turn_secret[0] != '\0') {
652 turn_credentials_t turn_creds;
653 asciichat_error_t turn_result =
654 turn_generate_credentials(session_string, config->turn_secret, 86400, &turn_creds);
655 if (turn_result == ASCIICHAT_OK) {
656 SAFE_STRNCPY(resp->turn_username, turn_creds.username, sizeof(resp->turn_username));
657 SAFE_STRNCPY(resp->turn_password, turn_creds.password, sizeof(resp->turn_password));
658 log_debug("Generated TURN credentials for session %s", session_string);
659 }
660 }
661
662 log_info("Participant joined session %s (participants=%d/%d, server=%s:%d, type=%s)", session_string,
663 session->current_participants + 1, session->max_participants, resp->server_address, resp->server_port,
664 session->session_type == SESSION_TYPE_WEBRTC ? "WebRTC" : "DirectTCP");
665 } else {
666 log_info("Participant joined session %s (participants=%d/%d, IP WITHHELD)", session_string,
667 session->current_participants + 1, session->max_participants);
668 }
669
670 session_entry_destroy(session);
671 return ASCIICHAT_OK;
672}
session_entry_t * database_session_find_by_string(sqlite3 *db, const char *session_string)
Find session by session_string.
@ ACIP_ERROR_INVALID_PASSWORD
Password verification failed.
Definition acds.h:1068
@ ACIP_ERROR_SESSION_NOT_FOUND
Session does not exist.
Definition acds.h:1066
@ ACIP_ERROR_SESSION_FULL
Session has reached max participants.
Definition acds.h:1067
@ ACIP_ERROR_NONE
No error (success)
Definition acds.h:1065
@ SESSION_TYPE_WEBRTC
WebRTC P2P mesh with STUN/TURN relay.
Definition acds.h:146
void session_entry_destroy(session_entry_t *entry)
Free a session entry and all its resources.
uint8_t participant_id[16]
char turn_secret[256]
Shared secret for TURN credential generation (HMAC-SHA1)
bool expose_ip_publicly
Allow IP disclosure without verification (explicit opt-in via –acds-expose-ip)
bool has_password
Password protection flag.
char server_address[64]
IPv4/IPv6 address or hostname.
uint8_t max_participants
1-8
uint8_t initiator_id[16]
First participant who created/joined the session.
uint8_t host_participant_id[16]
Current host's participant_id (valid if host_established)
bool host_established
Whether a host has been designated (false = still negotiating)
char session_string[(38+1)]
e.g., "affectionate-acquaintance-acquaintance"
uint8_t session_id[16]
UUID.
char password_hash[128]
Argon2id hash (if has_password)
uint8_t session_type
acds_session_type_t: 0=DIRECT_TCP, 1=WEBRTC
uint16_t server_port
Port number for client connection.
uint8_t current_participants
Active participant count.
TURN server credentials (username + password)
asciichat_error_t turn_generate_credentials(const char *session_id, const char *secret, uint32_t validity_seconds, turn_credentials_t *out_credentials)
Generate time-limited TURN credentials.

References ACIP_ERROR_INVALID_PASSWORD, ACIP_ERROR_NONE, ACIP_ERROR_SESSION_FULL, ACIP_ERROR_SESSION_NOT_FOUND, ACIP_MAX_SESSION_STRING_LEN, ASCIICHAT_OK, session_entry::current_participants, database_session_find_by_string(), ERROR_CONFIG, ERROR_INVALID_PARAM, session_entry::expose_ip_publicly, session_entry::has_password, session_entry::host_established, session_entry::host_participant_id, session_entry::initiator_id, log_debug, log_error, log_info, log_warn, session_entry::max_participants, participant_id, turn_credentials_t::password, session_entry::password_hash, SAFE_STRNCPY, session_entry::server_address, session_entry::server_port, session_entry_destroy(), session_entry::session_id, session_entry::session_string, session_entry::session_type, SESSION_TYPE_WEBRTC, SET_ERRNO, turn_generate_credentials(), acds_config_t::turn_secret, and turn_credentials_t::username.

◆ database_session_leave()

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

Leave session.

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

Definition at line 674 of file discovery/database.c.

674 {
675 if (!db || !session_id || !participant_id) {
676 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_id, or participant_id is NULL");
677 }
678
679 // Look up session_string from session_id
680 char session_string[SESSION_STRING_BUFFER_SIZE] = {0};
681 const char *lookup_sql = "SELECT session_string FROM sessions WHERE session_id = ?";
682 sqlite3_stmt *stmt = NULL;
683 int rc = sqlite3_prepare_v2(db, lookup_sql, -1, &stmt, NULL);
684 if (rc != SQLITE_OK) {
685 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session lookup: %s", sqlite3_errmsg(db));
686 }
687
688 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
689 rc = sqlite3_step(stmt);
690 if (rc != SQLITE_ROW) {
691 sqlite3_finalize(stmt);
692 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found");
693 }
694
695 const char *str = (const char *)sqlite3_column_text(stmt, 0);
696 if (str) {
697 SAFE_STRNCPY(session_string, str, sizeof(session_string));
698 }
699 sqlite3_finalize(stmt);
700
701 if (session_string[0] == '\0') {
702 return SET_ERRNO(ERROR_INVALID_STATE, "Session string is empty");
703 }
704
705 // Begin transaction
706 char *err_msg = NULL;
707 rc = sqlite3_exec(db, "BEGIN IMMEDIATE;", NULL, NULL, &err_msg);
708 if (rc != SQLITE_OK) {
709 log_error("Failed to begin transaction: %s", err_msg ? err_msg : "unknown");
710 sqlite3_free(err_msg);
711 return SET_ERRNO(ERROR_CONFIG, "Failed to begin transaction");
712 }
713
714 // Delete participant
715 const char *del_sql = "DELETE FROM participants WHERE participant_id = ? AND session_string = ?";
716 rc = sqlite3_prepare_v2(db, del_sql, -1, &stmt, NULL);
717 if (rc != SQLITE_OK) {
718 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
719 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare participant delete: %s", sqlite3_errmsg(db));
720 }
721
722 sqlite3_bind_blob(stmt, 1, participant_id, 16, SQLITE_STATIC);
723 sqlite3_bind_text(stmt, 2, session_string, -1, SQLITE_STATIC);
724 rc = sqlite3_step(stmt);
725 sqlite3_finalize(stmt);
726
727 if (rc != SQLITE_DONE) {
728 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
729 return SET_ERRNO(ERROR_CONFIG, "Failed to delete participant: %s", sqlite3_errmsg(db));
730 }
731
732 int changes = sqlite3_changes(db);
733 if (changes == 0) {
734 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
735 return SET_ERRNO(ERROR_INVALID_STATE, "Participant not in session");
736 }
737
738 uint64_t now = database_get_current_time_ms();
739
740 // Decrement participant count and update last activity
741 const char *update_sql = "UPDATE sessions SET current_participants = current_participants - 1, "
742 "last_activity_at = ? WHERE session_string = ? AND current_participants > 0";
743 rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL);
744 if (rc == SQLITE_OK) {
745 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now);
746 sqlite3_bind_text(stmt, 2, session_string, -1, SQLITE_STATIC);
747 sqlite3_step(stmt);
748 sqlite3_finalize(stmt);
749 }
750
751 // Check if session is now empty and delete if so
752 const char *check_sql = "SELECT current_participants FROM sessions WHERE session_string = ?";
753 rc = sqlite3_prepare_v2(db, check_sql, -1, &stmt, NULL);
754 if (rc == SQLITE_OK) {
755 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
756 if (sqlite3_step(stmt) == SQLITE_ROW) {
757 int count = sqlite3_column_int(stmt, 0);
758
759 if (count <= 0) {
760 log_info("Session %s has no participants, deleting", session_string);
761 sqlite3_finalize(stmt);
762
763 // Delete empty session
764 const char *del_session_sql = "DELETE FROM sessions WHERE session_string = ?";
765 rc = sqlite3_prepare_v2(db, del_session_sql, -1, &stmt, NULL);
766 if (rc == SQLITE_OK) {
767 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
768 sqlite3_step(stmt);
769 sqlite3_finalize(stmt);
770 }
771 stmt = NULL;
772 } else {
773 log_info("Participant left session %s (participants=%d remaining)", session_string, count);
774 }
775 }
776 if (stmt) {
777 sqlite3_finalize(stmt);
778 }
779 }
780
781 // Commit transaction
782 rc = sqlite3_exec(db, "COMMIT;", NULL, NULL, &err_msg);
783 if (rc != SQLITE_OK) {
784 log_error("Failed to commit transaction: %s", err_msg ? err_msg : "unknown");
785 sqlite3_free(err_msg);
786 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
787 return SET_ERRNO(ERROR_CONFIG, "Failed to commit transaction");
788 }
789
790 return ASCIICHAT_OK;
791}
#define SESSION_STRING_BUFFER_SIZE

References ASCIICHAT_OK, ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, log_error, log_info, participant_id, SAFE_STRNCPY, session_id, SESSION_STRING_BUFFER_SIZE, and SET_ERRNO.

◆ database_session_lookup()

asciichat_error_t database_session_lookup ( sqlite3 *  db,
const char *  session_string,
const acds_config_t *  config,
acip_session_info_t *  resp 
)

Lookup session by string.

Parameters
dbDatabase handle
session_stringSession string to look up
configACDS server configuration (for policy flags)
respSession info response (output)
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 420 of file discovery/database.c.

421 {
422 if (!db || !session_string || !config || !resp) {
423 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_string, config, or resp is NULL");
424 }
425
426 memset(resp, 0, sizeof(*resp));
427
428 const char *sql = "SELECT session_string, session_id, host_pubkey, password_hash, "
429 "max_participants, current_participants, capabilities, has_password, "
430 "expose_ip_publicly, session_type, server_address, server_port, "
431 "created_at, expires_at, last_activity_at FROM sessions WHERE session_string = ?";
432
433 sqlite3_stmt *stmt = NULL;
434 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
435 if (rc != SQLITE_OK) {
436 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session lookup: %s", sqlite3_errmsg(db));
437 }
438
439 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
440
441 rc = sqlite3_step(stmt);
442 if (rc != SQLITE_ROW) {
443 sqlite3_finalize(stmt);
444 resp->found = 0;
445 log_debug("Session lookup failed: %s (not found)", session_string);
446 return ASCIICHAT_OK;
447 }
448
449 // Load session data
450 resp->found = 1;
451
452 const void *blob = sqlite3_column_blob(stmt, 1);
453 if (blob) {
454 memcpy(resp->session_id, blob, 16);
455 }
456
457 blob = sqlite3_column_blob(stmt, 2);
458 if (blob) {
459 memcpy(resp->host_pubkey, blob, 32);
460 }
461
462 resp->max_participants = (uint8_t)sqlite3_column_int(stmt, 4);
463 resp->current_participants = (uint8_t)sqlite3_column_int(stmt, 5);
464 resp->capabilities = (uint8_t)sqlite3_column_int(stmt, 6);
465 resp->has_password = sqlite3_column_int(stmt, 7) != 0;
466 resp->session_type = (uint8_t)sqlite3_column_int(stmt, 9);
467 resp->created_at = (uint64_t)sqlite3_column_int64(stmt, 12);
468 resp->expires_at = (uint64_t)sqlite3_column_int64(stmt, 13);
469
470 // ACDS policy flags
471 resp->require_server_verify = config->require_server_verify ? 1 : 0;
472 resp->require_client_verify = config->require_client_verify ? 1 : 0;
473
474 sqlite3_finalize(stmt);
475
476 log_debug("Session lookup: %s (found, participants=%d/%d)", session_string, resp->current_participants,
477 resp->max_participants);
478
479 return ASCIICHAT_OK;
480}
bool require_server_verify
ACDS policy: require servers to verify client identity during handshake.
bool require_client_verify
ACDS policy: require clients to verify server identity during handshake.

References ASCIICHAT_OK, ERROR_CONFIG, ERROR_INVALID_PARAM, log_debug, acds_config_t::require_client_verify, acds_config_t::require_server_verify, and SET_ERRNO.

◆ database_session_revoke_key()

asciichat_error_t database_session_revoke_key ( sqlite3 *  db,
const char *  session_string,
const uint8_t  identity_pubkey[32] 
)

Revoke a key from a session.

Parameters
dbDatabase handle
session_stringSession string
identity_pubkeyEd25519 public key to revoke (32 bytes)
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 1080 of file discovery/database.c.

1081 {
1082 if (!db || !session_string || !identity_pubkey) {
1083 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for revoke_key");
1084 }
1085
1086 const char *sql = "UPDATE session_keys SET revoked = 1 WHERE session_string = ? AND identity_pubkey = ?";
1087
1088 sqlite3_stmt *stmt = NULL;
1089 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1090 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare revoke_key statement: %s", sqlite3_errmsg(db));
1091 }
1092
1093 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1094 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1095
1096 int rc = sqlite3_step(stmt);
1097 sqlite3_finalize(stmt);
1098
1099 if (rc != SQLITE_DONE) {
1100 return SET_ERRNO(ERROR_CONFIG, "Failed to revoke session key: %s", sqlite3_errmsg(db));
1101 }
1102
1103 log_debug("Revoked key from session %s", session_string);
1104 return ASCIICHAT_OK;
1105}

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

◆ database_session_start_migration()

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

Start host migration (discovery mode)

Called when the current host disconnects. Marks session as in_migration and starts a collection window for HOST_LOST candidates.

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

Definition at line 972 of file discovery/database.c.

972 {
973 if (!db || !session_id) {
974 return SET_ERRNO(ERROR_INVALID_PARAM, "db or session_id is NULL");
975 }
976
978
979 const char *sql = "UPDATE sessions SET "
980 "in_migration = 1, "
981 "migration_start_ns = ? "
982 "WHERE session_id = ?";
983
984 sqlite3_stmt *stmt = NULL;
985 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
986 if (rc != SQLITE_OK) {
987 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare migration start: %s", sqlite3_errmsg(db));
988 }
989
990 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now_ns);
991 sqlite3_bind_blob(stmt, 2, session_id, 16, SQLITE_STATIC);
992
993 rc = sqlite3_step(stmt);
994 sqlite3_finalize(stmt);
995
996 if (rc != SQLITE_DONE) {
997 return SET_ERRNO(ERROR_CONFIG, "Failed to start migration: %s", sqlite3_errmsg(db));
998 }
999
1000 int changes = sqlite3_changes(db);
1001 if (changes == 0) {
1002 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for migration start");
1003 }
1004
1005 log_info("Session migration started (session=%02x%02x..., start_ns=%" PRIu64 ")", session_id[0], session_id[1],
1006 now_ns);
1007
1008 return ASCIICHAT_OK;
1009}

References ASCIICHAT_OK, ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, log_info, session_id, SET_ERRNO, and time_get_realtime_ns().

◆ database_session_update_host()

asciichat_error_t database_session_update_host ( sqlite3 *  db,
const uint8_t  session_id[16],
const uint8_t  host_participant_id[16],
const char *  host_address,
uint16_t  host_port,
uint8_t  connection_type 
)

Update session host (discovery mode)

Called when a participant announces they are hosting. Updates the session's host fields so new joiners know where to connect.

Parameters
dbDatabase handle
session_idSession UUID
host_participant_idHost's participant UUID
host_addressHost's reachable address
host_portHost's port
connection_typeHow to reach host (ACIP_CONNECTION_TYPE_*)
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 885 of file discovery/database.c.

887 {
888 if (!db || !session_id || !host_participant_id) {
889 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_id, or host_participant_id is NULL");
890 }
891
892 uint64_t now = database_get_current_time_ms();
893
894 const char *sql = "UPDATE sessions SET "
895 "host_established = 1, "
896 "host_participant_id = ?, "
897 "host_address = ?, "
898 "host_port = ?, "
899 "host_connection_type = ?, "
900 "last_activity_at = ? "
901 "WHERE session_id = ?";
902
903 sqlite3_stmt *stmt = NULL;
904 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
905 if (rc != SQLITE_OK) {
906 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare host update: %s", sqlite3_errmsg(db));
907 }
908
909 sqlite3_bind_blob(stmt, 1, host_participant_id, 16, SQLITE_STATIC);
910 sqlite3_bind_text(stmt, 2, host_address ? host_address : "", -1, SQLITE_STATIC);
911 sqlite3_bind_int(stmt, 3, host_port);
912 sqlite3_bind_int(stmt, 4, connection_type);
913 sqlite3_bind_int64(stmt, 5, (sqlite3_int64)now);
914 sqlite3_bind_blob(stmt, 6, session_id, 16, SQLITE_STATIC);
915
916 rc = sqlite3_step(stmt);
917 sqlite3_finalize(stmt);
918
919 if (rc != SQLITE_DONE) {
920 return SET_ERRNO(ERROR_CONFIG, "Failed to update host: %s", sqlite3_errmsg(db));
921 }
922
923 int changes = sqlite3_changes(db);
924 if (changes == 0) {
925 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for host update");
926 }
927
928 log_info("Session host updated: participant=%02x%02x..., address=%s:%u, type=%d", host_participant_id[0],
929 host_participant_id[1], host_address ? host_address : "(none)", host_port, connection_type);
930
931 return ASCIICHAT_OK;
932}

References ASCIICHAT_OK, ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, log_info, session_id, and SET_ERRNO.

◆ database_session_verify_key()

bool database_session_verify_key ( sqlite3 *  db,
const char *  session_string,
const uint8_t  identity_pubkey[32] 
)

Check if a key is valid for a session (exists and not revoked)

Parameters
dbDatabase handle
session_stringSession string
identity_pubkeyEd25519 public key to check (32 bytes)
Returns
true if key is valid, false otherwise

Definition at line 1107 of file discovery/database.c.

1107 {
1108 if (!db || !session_string || !identity_pubkey) {
1109 return false;
1110 }
1111
1112 const char *sql = "SELECT 1 FROM session_keys WHERE session_string = ? AND identity_pubkey = ? AND revoked = 0";
1113
1114 sqlite3_stmt *stmt = NULL;
1115 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1116 return false;
1117 }
1118
1119 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1120 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1121
1122 bool valid = (sqlite3_step(stmt) == SQLITE_ROW);
1123 sqlite3_finalize(stmt);
1124
1125 return valid;
1126}
bool valid

References valid.

◆ database_stats_session_count()

asciichat_error_t database_stats_session_count ( sqlite3 *  db,
int64_t *  count_out 
)

Get total number of active sessions.

Parameters
dbDatabase handle
count_outOutput: number of active sessions
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 1164 of file discovery/database.c.

1164 {
1165 if (!db || !count_out) {
1166 return SET_ERRNO(ERROR_INVALID_PARAM, "db or count_out is NULL");
1167 }
1168
1169 const char *query = "SELECT COUNT(*) FROM sessions WHERE expires_at > ?";
1170 sqlite3_stmt *stmt;
1171 int rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL);
1172 if (rc != SQLITE_OK) {
1173 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare statement: %s", sqlite3_errmsg(db));
1174 }
1175
1176 time_t now = time(NULL);
1177 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now);
1178
1179 int64_t count = 0;
1180 rc = sqlite3_step(stmt);
1181 if (rc == SQLITE_ROW) {
1182 count = sqlite3_column_int64(stmt, 0);
1183 } else if (rc != SQLITE_DONE) {
1184 sqlite3_finalize(stmt);
1185 return SET_ERRNO(ERROR_CONFIG, "Failed to execute query: %s", sqlite3_errmsg(db));
1186 }
1187
1188 sqlite3_finalize(stmt);
1189 *count_out = count;
1190
1191 log_debug("Active session count: %lld", (long long)count);
1192 return ASCIICHAT_OK;
1193}

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

◆ database_stats_unique_clients()

asciichat_error_t database_stats_unique_clients ( sqlite3 *  db,
int64_t *  count_out 
)

Get total number of unique clients/participants.

Parameters
dbDatabase handle
count_outOutput: number of unique participants
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 1195 of file discovery/database.c.

1195 {
1196 if (!db || !count_out) {
1197 return SET_ERRNO(ERROR_INVALID_PARAM, "db or count_out is NULL");
1198 }
1199
1200 const char *query = "SELECT COUNT(DISTINCT participant_id) FROM participants";
1201 sqlite3_stmt *stmt;
1202 int rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL);
1203 if (rc != SQLITE_OK) {
1204 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare statement: %s", sqlite3_errmsg(db));
1205 }
1206
1207 int64_t count = 0;
1208 rc = sqlite3_step(stmt);
1209 if (rc == SQLITE_ROW) {
1210 count = sqlite3_column_int64(stmt, 0);
1211 } else if (rc != SQLITE_DONE) {
1212 sqlite3_finalize(stmt);
1213 return SET_ERRNO(ERROR_CONFIG, "Failed to execute query: %s", sqlite3_errmsg(db));
1214 }
1215
1216 sqlite3_finalize(stmt);
1217 *count_out = count;
1218
1219 log_debug("Unique clients count: %lld", (long long)count);
1220 return ASCIICHAT_OK;
1221}

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