1429 lines
48 KiB
C
1429 lines
48 KiB
C
#define _GNU_SOURCE
|
|
|
|
#include "sqlite_db_ops.h"
|
|
#include "debug.h"
|
|
#include "config.h"
|
|
|
|
#include <sqlite3.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
// Database handle owned by sqlite_db_ops.
|
|
static sqlite3* g_db = NULL;
|
|
extern char g_database_path[512];
|
|
|
|
// Optional per-thread connection override (used by thread pool workers)
|
|
static __thread sqlite3* g_thread_db = NULL;
|
|
|
|
static sqlite3* sqlite_db_active_connection(void) {
|
|
return g_thread_db ? g_thread_db : g_db;
|
|
}
|
|
|
|
typedef struct sqlite_db_stmt {
|
|
sqlite3_stmt* stmt;
|
|
} sqlite_db_stmt_t;
|
|
|
|
int sqlite_db_init(const char* connection_string) {
|
|
if (!connection_string || connection_string[0] == '\0') return DB_MISUSE;
|
|
if (g_db) return DB_OK;
|
|
|
|
int rc = sqlite3_open(connection_string, &g_db);
|
|
if (rc != SQLITE_OK) {
|
|
if (g_db) {
|
|
sqlite3_close(g_db);
|
|
g_db = NULL;
|
|
}
|
|
return DB_ERROR;
|
|
}
|
|
|
|
strncpy(g_database_path, connection_string, sizeof(g_database_path) - 1);
|
|
g_database_path[sizeof(g_database_path) - 1] = '\0';
|
|
return DB_OK;
|
|
}
|
|
|
|
void sqlite_db_close(void) {
|
|
if (!g_db) return;
|
|
sqlite3_close(g_db);
|
|
g_db = NULL;
|
|
}
|
|
|
|
int sqlite_db_set_thread_connection(void* connection) {
|
|
g_thread_db = (sqlite3*)connection;
|
|
return DB_OK;
|
|
}
|
|
|
|
void sqlite_db_clear_thread_connection(void) {
|
|
g_thread_db = NULL;
|
|
}
|
|
|
|
int sqlite_db_open_worker_connection(const char* sqlite_db_path, void** out_connection) {
|
|
if (!out_connection) return -1;
|
|
*out_connection = NULL;
|
|
|
|
const char* effective_path = (sqlite_db_path && sqlite_db_path[0] != '\0') ? sqlite_db_path : g_database_path;
|
|
if (!effective_path || effective_path[0] == '\0') return -1;
|
|
|
|
sqlite3* db = NULL;
|
|
int rc = sqlite3_open_v2(effective_path, &db, SQLITE_OPEN_READWRITE, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
if (db) sqlite3_close(db);
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);
|
|
sqlite3_exec(db, "PRAGMA wal_autocheckpoint=0;", NULL, NULL, NULL);
|
|
sqlite3_busy_timeout(db, 5000);
|
|
sqlite_db_set_thread_connection(db);
|
|
*out_connection = (void*)db;
|
|
return 0;
|
|
}
|
|
|
|
// SQLite has no LISTEN/NOTIFY mechanism; stubs return -1 (unsupported) so the
|
|
// api-worker falls back to timer-based polling.
|
|
int sqlite_db_worker_listen(void* connection, const char* channel) {
|
|
(void)connection; (void)channel; return -1;
|
|
}
|
|
int sqlite_db_worker_poll_notify(void* connection, int timeout_ms) {
|
|
(void)connection; (void)timeout_ms; return -1;
|
|
}
|
|
|
|
void sqlite_db_close_worker_connection(void* connection) {
|
|
if (!connection) return;
|
|
sqlite_db_clear_thread_connection();
|
|
sqlite3_close((sqlite3*)connection);
|
|
}
|
|
|
|
int sqlite_db_is_available(void) {
|
|
return sqlite_db_active_connection() != NULL;
|
|
}
|
|
|
|
const char* sqlite_db_last_error(void) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db) return "database not available";
|
|
return sqlite3_errmsg(db);
|
|
}
|
|
|
|
const char* sqlite_db_get_database_path(void) {
|
|
return g_database_path;
|
|
}
|
|
|
|
int sqlite_db_prepare(const char* sql, sqlite_db_stmt_t** out_stmt) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !sql || !out_stmt) return DB_MISUSE;
|
|
|
|
sqlite3_stmt* raw_stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(db, sql, -1, &raw_stmt, NULL);
|
|
if (rc != SQLITE_OK) return rc;
|
|
|
|
sqlite_db_stmt_t* wrapper = (sqlite_db_stmt_t*)malloc(sizeof(sqlite_db_stmt_t));
|
|
if (!wrapper) {
|
|
sqlite3_finalize(raw_stmt);
|
|
return DB_ERROR;
|
|
}
|
|
|
|
wrapper->stmt = raw_stmt;
|
|
*out_stmt = wrapper;
|
|
return DB_OK;
|
|
}
|
|
|
|
int sqlite_db_bind_text_param(sqlite_db_stmt_t* stmt, int index, const char* value) {
|
|
if (!stmt || !stmt->stmt) return DB_MISUSE;
|
|
return sqlite3_bind_text(stmt->stmt, index, value ? value : "", -1, SQLITE_TRANSIENT);
|
|
}
|
|
|
|
int sqlite_db_bind_int_param(sqlite_db_stmt_t* stmt, int index, int value) {
|
|
if (!stmt || !stmt->stmt) return DB_MISUSE;
|
|
return sqlite3_bind_int(stmt->stmt, index, value);
|
|
}
|
|
|
|
int sqlite_db_bind_int64_param(sqlite_db_stmt_t* stmt, int index, long long value) {
|
|
if (!stmt || !stmt->stmt) return DB_MISUSE;
|
|
return sqlite3_bind_int64(stmt->stmt, index, (sqlite3_int64)value);
|
|
}
|
|
|
|
int sqlite_db_step_stmt(sqlite_db_stmt_t* stmt) {
|
|
if (!stmt || !stmt->stmt) return DB_MISUSE;
|
|
return sqlite3_step(stmt->stmt);
|
|
}
|
|
|
|
int sqlite_db_reset_stmt(sqlite_db_stmt_t* stmt) {
|
|
if (!stmt || !stmt->stmt) return DB_MISUSE;
|
|
return sqlite3_reset(stmt->stmt);
|
|
}
|
|
|
|
const char* sqlite_db_column_text_value(sqlite_db_stmt_t* stmt, int col) {
|
|
if (!stmt || !stmt->stmt) return NULL;
|
|
return (const char*)sqlite3_column_text(stmt->stmt, col);
|
|
}
|
|
|
|
int sqlite_db_column_int_value(sqlite_db_stmt_t* stmt, int col) {
|
|
if (!stmt || !stmt->stmt) return 0;
|
|
return sqlite3_column_int(stmt->stmt, col);
|
|
}
|
|
|
|
long long sqlite_db_column_int64_value(sqlite_db_stmt_t* stmt, int col) {
|
|
if (!stmt || !stmt->stmt) return 0;
|
|
return (long long)sqlite3_column_int64(stmt->stmt, col);
|
|
}
|
|
|
|
double sqlite_db_column_double_value(sqlite_db_stmt_t* stmt, int col) {
|
|
if (!stmt || !stmt->stmt) return 0.0;
|
|
return sqlite3_column_double(stmt->stmt, col);
|
|
}
|
|
|
|
void sqlite_db_finalize_stmt(sqlite_db_stmt_t* stmt) {
|
|
if (!stmt) return;
|
|
if (stmt->stmt) sqlite3_finalize(stmt->stmt);
|
|
free(stmt);
|
|
}
|
|
|
|
int sqlite_db_log_subscription_created(const char* sub_id, const char* wsi_ptr,
|
|
const char* client_ip, const char* filter_json) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !sub_id || !wsi_ptr || !client_ip) return -1;
|
|
|
|
const char* sql =
|
|
"INSERT OR REPLACE INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type, filter_json) "
|
|
"VALUES (?, ?, ?, 'created', ?)";
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, wsi_ptr, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, client_ip, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 4, filter_json ? filter_json : "[]", -1, SQLITE_TRANSIENT);
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
|
}
|
|
|
|
int sqlite_db_log_subscription_closed(const char* sub_id, const char* client_ip) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !sub_id) return -1;
|
|
|
|
const char* insert_sql =
|
|
"INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) "
|
|
"VALUES (?, '', ?, 'closed')";
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) {
|
|
sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, client_ip ? client_ip : "unknown", -1, SQLITE_TRANSIENT);
|
|
sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
|
|
const char* update_sql =
|
|
"UPDATE subscriptions "
|
|
"SET ended_at = strftime('%s', 'now') "
|
|
"WHERE subscription_id = ? AND event_type = 'created' AND ended_at IS NULL";
|
|
|
|
if (sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
|
sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT);
|
|
int rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
|
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
|
}
|
|
|
|
int sqlite_db_log_subscription_disconnected(const char* client_ip) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !client_ip) return -1;
|
|
|
|
const char* update_sql =
|
|
"UPDATE subscriptions "
|
|
"SET ended_at = strftime('%s', 'now') "
|
|
"WHERE client_ip = ? AND event_type = 'created' AND ended_at IS NULL";
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_TRANSIENT);
|
|
int rc = sqlite3_step(stmt);
|
|
int changes = sqlite3_changes(db);
|
|
sqlite3_finalize(stmt);
|
|
if (rc != SQLITE_DONE) return -1;
|
|
|
|
if (changes > 0) {
|
|
const char* insert_sql =
|
|
"INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) "
|
|
"VALUES ('disconnect', '', ?, 'disconnected')";
|
|
|
|
if (sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) {
|
|
sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_TRANSIENT);
|
|
sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
}
|
|
|
|
return changes;
|
|
}
|
|
|
|
int sqlite_db_update_subscription_events_sent(const char* sub_id, int events_sent) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !sub_id) return -1;
|
|
|
|
const char* sql =
|
|
"UPDATE subscriptions "
|
|
"SET events_sent = ? "
|
|
"WHERE subscription_id = ? AND event_type = 'created'";
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_int(stmt, 1, events_sent);
|
|
sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_TRANSIENT);
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
|
}
|
|
|
|
int sqlite_db_cleanup_orphaned_subscriptions(void) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db) return -1;
|
|
|
|
const char* sql =
|
|
"UPDATE subscriptions "
|
|
"SET ended_at = strftime('%s', 'now') "
|
|
"WHERE event_type = 'created' AND ended_at IS NULL";
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
int changes = sqlite3_changes(db);
|
|
sqlite3_finalize(stmt);
|
|
|
|
return (rc == SQLITE_DONE) ? changes : -1;
|
|
}
|
|
|
|
int sqlite_db_get_event_pubkey(const char* event_id, char* pubkey_out, size_t pubkey_out_size) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !event_id || !pubkey_out || pubkey_out_size == 0) return -1;
|
|
|
|
const char* sql = "SELECT pubkey FROM events WHERE id = ?";
|
|
sqlite3_stmt* stmt = NULL;
|
|
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_TRANSIENT);
|
|
int rc = sqlite3_step(stmt);
|
|
if (rc == SQLITE_ROW) {
|
|
const char* pubkey = (const char*)sqlite3_column_text(stmt, 0);
|
|
if (pubkey) {
|
|
snprintf(pubkey_out, pubkey_out_size, "%s", pubkey);
|
|
sqlite3_finalize(stmt);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return 0;
|
|
}
|
|
|
|
int sqlite_db_delete_event_by_id(const char* event_id, const char* requester_pubkey) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !event_id || !requester_pubkey) return -1;
|
|
|
|
const char* sql = "DELETE FROM events WHERE id = ? AND pubkey = ?";
|
|
sqlite3_stmt* stmt = NULL;
|
|
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, requester_pubkey, -1, SQLITE_TRANSIENT);
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
int changes = sqlite3_changes(db);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (rc != SQLITE_DONE) return -1;
|
|
return changes;
|
|
}
|
|
|
|
int sqlite_db_delete_events_by_address(const char* pubkey, int kind,
|
|
const char* d_tag, long before_timestamp) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !pubkey) return -1;
|
|
|
|
const char* sql_with_d =
|
|
"DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ? "
|
|
"AND json_extract(tags, '$[*]') LIKE '%[\"d\",\"' || ? || '\"]%'";
|
|
const char* sql_no_d =
|
|
"DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ?";
|
|
|
|
const int has_d = (d_tag && d_tag[0] != '\0');
|
|
sqlite3_stmt* stmt = NULL;
|
|
|
|
if (sqlite3_prepare_v2(db, has_d ? sql_with_d : sql_no_d, -1, &stmt, NULL) != SQLITE_OK) {
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_bind_int(stmt, 1, kind);
|
|
sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int64(stmt, 3, before_timestamp);
|
|
if (has_d) {
|
|
sqlite3_bind_text(stmt, 4, d_tag, -1, SQLITE_TRANSIENT);
|
|
}
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
int changes = sqlite3_changes(db);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (rc != SQLITE_DONE) return -1;
|
|
return changes;
|
|
}
|
|
|
|
static int sqlite_db_scalar_exists_open(sqlite3* db, const char* sql, const char* value) {
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0;
|
|
|
|
if (value) sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT);
|
|
|
|
int exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0;
|
|
sqlite3_finalize(stmt);
|
|
return exists;
|
|
}
|
|
|
|
int sqlite_db_is_pubkey_blacklisted(const char* pubkey) {
|
|
if (!pubkey || g_database_path[0] == '\0') return 0;
|
|
|
|
sqlite3* db = NULL;
|
|
if (sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) return 0;
|
|
|
|
const char* sql =
|
|
"SELECT 1 FROM auth_rules WHERE rule_type = 'blacklist' "
|
|
"AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1";
|
|
|
|
int exists = sqlite_db_scalar_exists_open(db, sql, pubkey);
|
|
sqlite3_close(db);
|
|
return exists;
|
|
}
|
|
|
|
int sqlite_db_is_hash_blacklisted(const char* resource_hash) {
|
|
if (!resource_hash || g_database_path[0] == '\0') return 0;
|
|
|
|
sqlite3* db = NULL;
|
|
if (sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) return 0;
|
|
|
|
const char* sql =
|
|
"SELECT 1 FROM auth_rules WHERE rule_type = 'blacklist' "
|
|
"AND pattern_type = 'hash' AND pattern_value = ? AND active = 1 LIMIT 1";
|
|
|
|
int exists = sqlite_db_scalar_exists_open(db, sql, resource_hash);
|
|
sqlite3_close(db);
|
|
return exists;
|
|
}
|
|
|
|
int sqlite_db_is_pubkey_whitelisted(const char* pubkey) {
|
|
if (!pubkey || g_database_path[0] == '\0') return 0;
|
|
|
|
sqlite3* db = NULL;
|
|
if (sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) return 0;
|
|
|
|
const char* sql =
|
|
"SELECT 1 FROM auth_rules WHERE rule_type IN ('whitelist', 'wot_whitelist') "
|
|
"AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1";
|
|
|
|
int exists = sqlite_db_scalar_exists_open(db, sql, pubkey);
|
|
sqlite3_close(db);
|
|
return exists;
|
|
}
|
|
|
|
int sqlite_db_count_active_whitelist_rules(void) {
|
|
if (g_database_path[0] == '\0') return 0;
|
|
|
|
sqlite3* db = NULL;
|
|
sqlite3_stmt* stmt = NULL;
|
|
int count = 0;
|
|
|
|
if (sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) return 0;
|
|
|
|
const char* sql =
|
|
"SELECT COUNT(*) FROM auth_rules WHERE rule_type IN ('whitelist', 'wot_whitelist') "
|
|
"AND pattern_type = 'pubkey' AND active = 1 LIMIT 1";
|
|
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
count = sqlite3_column_int(stmt, 0);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
|
|
sqlite3_close(db);
|
|
return count;
|
|
}
|
|
|
|
int sqlite_db_count_with_sql(const char* sql, const char** bind_params, int bind_param_count, int* out_count) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !sql || !out_count) return -1;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
return -1;
|
|
}
|
|
|
|
for (int i = 0; i < bind_param_count; i++) {
|
|
const char* param = (bind_params && bind_params[i]) ? bind_params[i] : "";
|
|
sqlite3_bind_text(stmt, i + 1, param, -1, SQLITE_TRANSIENT);
|
|
}
|
|
|
|
int count = 0;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
count = sqlite3_column_int(stmt, 0);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
*out_count = count;
|
|
return 0;
|
|
}
|
|
|
|
char* sqlite_db_execute_readonly_query_json(const char* query, const char* request_id,
|
|
char* error_message, size_t error_size,
|
|
int max_rows, int timeout_ms) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !query || !request_id || !error_message) return NULL;
|
|
|
|
sqlite3_busy_timeout(db, timeout_ms > 0 ? timeout_ms : 5000);
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
const char* err_msg = sqlite3_errmsg(db);
|
|
snprintf(error_message, error_size, "SQL prepare failed: %s", err_msg ? err_msg : "unknown");
|
|
return NULL;
|
|
}
|
|
|
|
cJSON* response = cJSON_CreateObject();
|
|
if (!response) {
|
|
sqlite3_finalize(stmt);
|
|
snprintf(error_message, error_size, "Failed to allocate query response object");
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddStringToObject(response, "query_type", "sql_query");
|
|
cJSON_AddStringToObject(response, "request_id", request_id);
|
|
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
|
|
cJSON_AddStringToObject(response, "query", query);
|
|
|
|
int col_count = sqlite3_column_count(stmt);
|
|
cJSON* columns = cJSON_CreateArray();
|
|
if (!columns) {
|
|
sqlite3_finalize(stmt);
|
|
cJSON_Delete(response);
|
|
snprintf(error_message, error_size, "Failed to allocate columns array");
|
|
return NULL;
|
|
}
|
|
for (int i = 0; i < col_count; i++) {
|
|
const char* col_name = sqlite3_column_name(stmt, i);
|
|
cJSON_AddItemToArray(columns, cJSON_CreateString(col_name ? col_name : ""));
|
|
}
|
|
cJSON_AddItemToObject(response, "columns", columns);
|
|
|
|
cJSON* rows = cJSON_CreateArray();
|
|
if (!rows) {
|
|
sqlite3_finalize(stmt);
|
|
cJSON_Delete(response);
|
|
snprintf(error_message, error_size, "Failed to allocate rows array");
|
|
return NULL;
|
|
}
|
|
|
|
const int row_limit = (max_rows > 0) ? max_rows : 1000;
|
|
int row_count = 0;
|
|
|
|
struct timespec start_time;
|
|
clock_gettime(CLOCK_MONOTONIC, &start_time);
|
|
|
|
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW && row_count < row_limit) {
|
|
cJSON* row = cJSON_CreateArray();
|
|
if (!row) {
|
|
sqlite3_finalize(stmt);
|
|
cJSON_Delete(rows);
|
|
cJSON_Delete(response);
|
|
snprintf(error_message, error_size, "Failed to allocate row array");
|
|
return NULL;
|
|
}
|
|
|
|
for (int i = 0; i < col_count; i++) {
|
|
int col_type = sqlite3_column_type(stmt, i);
|
|
switch (col_type) {
|
|
case SQLITE_INTEGER:
|
|
cJSON_AddItemToArray(row, cJSON_CreateNumber((double)sqlite3_column_int64(stmt, i)));
|
|
break;
|
|
case SQLITE_FLOAT:
|
|
cJSON_AddItemToArray(row, cJSON_CreateNumber(sqlite3_column_double(stmt, i)));
|
|
break;
|
|
case SQLITE_TEXT: {
|
|
const char* text = (const char*)sqlite3_column_text(stmt, i);
|
|
cJSON_AddItemToArray(row, cJSON_CreateString(text ? text : ""));
|
|
break;
|
|
}
|
|
case SQLITE_BLOB: {
|
|
const void* blob = sqlite3_column_blob(stmt, i);
|
|
int blob_size = sqlite3_column_bytes(stmt, i);
|
|
if (blob && blob_size > 0) {
|
|
char* hex_str = malloc((size_t)blob_size * 2 + 1);
|
|
if (hex_str) {
|
|
for (int j = 0; j < blob_size; j++) {
|
|
sprintf(hex_str + j * 2, "%02x", ((const unsigned char*)blob)[j]);
|
|
}
|
|
hex_str[(size_t)blob_size * 2] = '\0';
|
|
cJSON_AddItemToArray(row, cJSON_CreateString(hex_str));
|
|
free(hex_str);
|
|
} else {
|
|
cJSON_AddItemToArray(row, cJSON_CreateString("[BLOB]"));
|
|
}
|
|
} else {
|
|
cJSON_AddItemToArray(row, cJSON_CreateString(""));
|
|
}
|
|
break;
|
|
}
|
|
case SQLITE_NULL:
|
|
cJSON_AddItemToArray(row, cJSON_CreateNull());
|
|
break;
|
|
default:
|
|
cJSON_AddItemToArray(row, cJSON_CreateString("[UNKNOWN]"));
|
|
break;
|
|
}
|
|
}
|
|
|
|
cJSON_AddItemToArray(rows, row);
|
|
row_count++;
|
|
|
|
struct timespec current_time;
|
|
clock_gettime(CLOCK_MONOTONIC, ¤t_time);
|
|
double elapsed = (current_time.tv_sec - start_time.tv_sec) +
|
|
(current_time.tv_nsec - start_time.tv_nsec) / 1e9;
|
|
if (elapsed > 4.5) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (rc != SQLITE_DONE && rc != SQLITE_ROW) {
|
|
const char* err_msg = sqlite3_errmsg(db);
|
|
snprintf(error_message, error_size, "SQL execution failed: %s", err_msg ? err_msg : "unknown");
|
|
cJSON_Delete(rows);
|
|
cJSON_Delete(response);
|
|
return NULL;
|
|
}
|
|
|
|
if (row_count >= row_limit) {
|
|
cJSON_AddStringToObject(response, "warning", "Result truncated to maximum row limit");
|
|
}
|
|
|
|
cJSON_AddNumberToObject(response, "row_count", row_count);
|
|
cJSON_AddNumberToObject(response, "execution_time_ms", 0);
|
|
cJSON_AddItemToObject(response, "rows", rows);
|
|
|
|
char* json_result = cJSON_Print(response);
|
|
cJSON_Delete(response);
|
|
|
|
if (!json_result) {
|
|
snprintf(error_message, error_size, "Failed to generate JSON response");
|
|
return NULL;
|
|
}
|
|
|
|
return json_result;
|
|
}
|
|
|
|
int sqlite_db_get_total_event_count_ll(long long* out_count) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !out_count) return -1;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "SELECT COUNT(*) FROM events";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
|
|
|
long long count = 0;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
count = sqlite3_column_int64(stmt, 0);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
*out_count = count;
|
|
return 0;
|
|
}
|
|
|
|
int sqlite_db_get_event_count_since(time_t cutoff, long long* out_count) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !out_count) return -1;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "SELECT COUNT(*) FROM events WHERE created_at >= ?";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff);
|
|
|
|
long long count = 0;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
count = sqlite3_column_int64(stmt, 0);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
*out_count = count;
|
|
return 0;
|
|
}
|
|
|
|
int sqlite_db_get_storage_size_bytes(long long* out_size) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
const char* db_path = sqlite_db_get_database_path();
|
|
if (!db || !out_size || !db_path || db_path[0] == '\0') return -1;
|
|
|
|
long long total_size = 0;
|
|
struct stat db_stat;
|
|
if (stat(db_path, &db_stat) == 0) {
|
|
total_size += (long long)db_stat.st_size;
|
|
}
|
|
|
|
char wal_path[1024];
|
|
snprintf(wal_path, sizeof(wal_path), "%s-wal", db_path);
|
|
if (stat(wal_path, &db_stat) == 0) {
|
|
total_size += (long long)db_stat.st_size;
|
|
}
|
|
|
|
*out_size = total_size;
|
|
return 0;
|
|
}
|
|
|
|
cJSON* sqlite_db_get_event_kind_distribution_rows(long long* out_total_events) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db) return NULL;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "SELECT kind, COUNT(*) as count FROM events GROUP BY kind ORDER BY count DESC";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
|
|
|
cJSON* rows = cJSON_CreateArray();
|
|
if (!rows) {
|
|
sqlite3_finalize(stmt);
|
|
return NULL;
|
|
}
|
|
|
|
long long total = 0;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
cJSON* row = cJSON_CreateObject();
|
|
if (!row) {
|
|
sqlite3_finalize(stmt);
|
|
cJSON_Delete(rows);
|
|
return NULL;
|
|
}
|
|
|
|
int kind = sqlite3_column_int(stmt, 0);
|
|
long long count = sqlite3_column_int64(stmt, 1);
|
|
total += count;
|
|
|
|
cJSON_AddNumberToObject(row, "kind", kind);
|
|
cJSON_AddNumberToObject(row, "count", count);
|
|
cJSON_AddItemToArray(rows, row);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
if (out_total_events) {
|
|
*out_total_events = total;
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
cJSON* sqlite_db_get_top_pubkeys_rows(int limit) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db) return NULL;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "SELECT pubkey, COUNT(*) as count FROM events GROUP BY pubkey ORDER BY count DESC LIMIT ?";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
|
|
|
sqlite3_bind_int(stmt, 1, limit > 0 ? limit : 10);
|
|
|
|
cJSON* rows = cJSON_CreateArray();
|
|
if (!rows) {
|
|
sqlite3_finalize(stmt);
|
|
return NULL;
|
|
}
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
cJSON* row = cJSON_CreateObject();
|
|
if (!row) {
|
|
sqlite3_finalize(stmt);
|
|
cJSON_Delete(rows);
|
|
return NULL;
|
|
}
|
|
|
|
const char* pubkey = (const char*)sqlite3_column_text(stmt, 0);
|
|
long long count = sqlite3_column_int64(stmt, 1);
|
|
cJSON_AddStringToObject(row, "pubkey", pubkey ? pubkey : "");
|
|
cJSON_AddNumberToObject(row, "count", count);
|
|
cJSON_AddItemToArray(rows, row);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return rows;
|
|
}
|
|
|
|
cJSON* sqlite_db_get_subscription_details_rows(void) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db) return NULL;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql =
|
|
"SELECT * "
|
|
"FROM active_subscriptions_log "
|
|
"ORDER BY created_at DESC";
|
|
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
|
|
|
cJSON* rows = cJSON_CreateArray();
|
|
if (!rows) {
|
|
sqlite3_finalize(stmt);
|
|
return NULL;
|
|
}
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
cJSON* row = cJSON_CreateObject();
|
|
if (!row) {
|
|
sqlite3_finalize(stmt);
|
|
cJSON_Delete(rows);
|
|
return NULL;
|
|
}
|
|
|
|
const char* sub_id = (const char*)sqlite3_column_text(stmt, 0);
|
|
const char* client_ip = (const char*)sqlite3_column_text(stmt, 1);
|
|
const char* filter_json = (const char*)sqlite3_column_text(stmt, 2);
|
|
long long events_sent = sqlite3_column_int64(stmt, 3);
|
|
long long created_at = sqlite3_column_int64(stmt, 4);
|
|
long long duration_seconds = sqlite3_column_int64(stmt, 5);
|
|
const char* wsi_pointer = (const char*)sqlite3_column_text(stmt, 6);
|
|
|
|
cJSON_AddStringToObject(row, "id", sub_id ? sub_id : "");
|
|
cJSON_AddStringToObject(row, "client_ip", client_ip ? client_ip : "");
|
|
cJSON_AddStringToObject(row, "filter_json", filter_json ? filter_json : "[]");
|
|
cJSON_AddNumberToObject(row, "events_sent", events_sent);
|
|
cJSON_AddNumberToObject(row, "created_at", created_at);
|
|
cJSON_AddNumberToObject(row, "duration_seconds", duration_seconds);
|
|
cJSON_AddStringToObject(row, "wsi_pointer", wsi_pointer ? wsi_pointer : "");
|
|
|
|
cJSON_AddItemToArray(rows, row);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return rows;
|
|
}
|
|
|
|
cJSON* sqlite_db_get_all_config_rows(void) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db) return NULL;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "SELECT key, value FROM config ORDER BY key";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
|
|
|
cJSON* rows = cJSON_CreateArray();
|
|
if (!rows) {
|
|
sqlite3_finalize(stmt);
|
|
return NULL;
|
|
}
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
cJSON* row = cJSON_CreateObject();
|
|
if (!row) {
|
|
sqlite3_finalize(stmt);
|
|
cJSON_Delete(rows);
|
|
return NULL;
|
|
}
|
|
|
|
const char* key = (const char*)sqlite3_column_text(stmt, 0);
|
|
const char* value = (const char*)sqlite3_column_text(stmt, 1);
|
|
cJSON_AddStringToObject(row, "key", key ? key : "");
|
|
cJSON_AddStringToObject(row, "value", value ? value : "");
|
|
cJSON_AddItemToArray(rows, row);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return rows;
|
|
}
|
|
|
|
char* sqlite_db_get_config_value_dup(const char* key) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !key) return NULL;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "SELECT value FROM config WHERE key = ?";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
|
|
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT);
|
|
|
|
char* result = NULL;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char* value = (const char*)sqlite3_column_text(stmt, 0);
|
|
if (value) result = strdup(value);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return result;
|
|
}
|
|
|
|
int sqlite_db_set_config_value_full(const char* key, const char* value, const char* data_type,
|
|
const char* description, const char* category, int requires_restart) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !key || !value || !data_type) return -1;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql =
|
|
"INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) "
|
|
"VALUES (?, ?, ?, ?, ?, ?)";
|
|
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, value, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 4, description ? description : "", -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 5, category ? category : "general", -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int(stmt, 6, requires_restart);
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
|
}
|
|
|
|
int sqlite_db_update_config_value_only(const char* key, const char* value) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !key || !value) return -1;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "UPDATE config SET value = ?, updated_at = strftime('%s', 'now') WHERE key = ?";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, key, -1, SQLITE_TRANSIENT);
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
|
}
|
|
|
|
int sqlite_db_upsert_config_value(const char* key, const char* value, const char* data_type) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !key || !value || !data_type) return -1;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type) VALUES (?, ?, ?)";
|
|
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, value, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_TRANSIENT);
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
|
}
|
|
|
|
int sqlite_db_store_relay_private_key_hex(const char* relay_privkey_hex) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !relay_privkey_hex) return -1;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "INSERT OR REPLACE INTO relay_seckey (private_key_hex) VALUES (?)";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_text(stmt, 1, relay_privkey_hex, -1, SQLITE_TRANSIENT);
|
|
int rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
|
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
|
}
|
|
|
|
char* sqlite_db_get_relay_private_key_hex_dup(void) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db) return NULL;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "SELECT private_key_hex FROM relay_seckey";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
|
|
|
char* result = NULL;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char* key_from_db = (const char*)sqlite3_column_text(stmt, 0);
|
|
if (key_from_db) result = strdup(key_from_db);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return result;
|
|
}
|
|
|
|
int sqlite_db_store_config_event(const cJSON* event) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !event) return -1;
|
|
|
|
cJSON* id_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "id");
|
|
cJSON* pubkey_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "pubkey");
|
|
cJSON* created_at_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "created_at");
|
|
cJSON* kind_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "kind");
|
|
cJSON* content_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "content");
|
|
cJSON* sig_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "sig");
|
|
cJSON* tags_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "tags");
|
|
if (!id_obj || !pubkey_obj || !created_at_obj || !kind_obj || !content_obj || !sig_obj || !tags_obj) {
|
|
return -1;
|
|
}
|
|
|
|
char* tags_str = cJSON_Print(tags_obj);
|
|
if (!tags_str) return -1;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
free(tags_str);
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, cJSON_GetStringValue(id_obj), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(pubkey_obj), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at_obj));
|
|
sqlite3_bind_int(stmt, 4, (int)cJSON_GetNumberValue(kind_obj));
|
|
sqlite3_bind_text(stmt, 5, "regular", -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 6, cJSON_GetStringValue(content_obj), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 7, cJSON_GetStringValue(sig_obj), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 8, tags_str, -1, SQLITE_TRANSIENT);
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
free(tags_str);
|
|
|
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
|
}
|
|
|
|
static int sqlite_db_insert_event_tags_json_with_db(sqlite3* db, const char* event_id, const char* tags_json) {
|
|
if (!db || !event_id || !tags_json) {
|
|
return -1;
|
|
}
|
|
|
|
cJSON* tags = cJSON_Parse(tags_json);
|
|
if (!tags || !cJSON_IsArray(tags)) {
|
|
if (tags) cJSON_Delete(tags);
|
|
return 0;
|
|
}
|
|
|
|
const char* sql = "INSERT INTO event_tags (event_id, tag_name, tag_value, tag_index) VALUES (?, ?, ?, ?)";
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
cJSON_Delete(tags);
|
|
return -1;
|
|
}
|
|
|
|
int tag_index = 0;
|
|
cJSON* tag = NULL;
|
|
cJSON_ArrayForEach(tag, tags) {
|
|
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
|
cJSON* name = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* value = cJSON_GetArrayItem(tag, 1);
|
|
if (cJSON_IsString(name) && cJSON_IsString(value)) {
|
|
sqlite3_reset(stmt);
|
|
sqlite3_clear_bindings(stmt);
|
|
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC);
|
|
sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(name), -1, SQLITE_STATIC);
|
|
sqlite3_bind_text(stmt, 3, cJSON_GetStringValue(value), -1, SQLITE_STATIC);
|
|
sqlite3_bind_int(stmt, 4, tag_index);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
DEBUG_WARN("Failed to insert event tag for %s: %s", event_id, sqlite3_errmsg(db));
|
|
}
|
|
}
|
|
}
|
|
tag_index++;
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
cJSON_Delete(tags);
|
|
return 0;
|
|
}
|
|
|
|
int sqlite_db_insert_event_with_json(const char* id, const char* pubkey, long long created_at,
|
|
int kind, const char* event_type, const char* content,
|
|
const char* sig, const char* tags_json, const char* event_json,
|
|
int* out_step_rc, int* out_extended_errcode) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !id || !pubkey || !event_type || !content || !sig || !tags_json || !event_json) {
|
|
return -1;
|
|
}
|
|
|
|
const char* sql =
|
|
"INSERT INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags, event_json) "
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, id, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int64(stmt, 3, (sqlite3_int64)created_at);
|
|
sqlite3_bind_int(stmt, 4, kind);
|
|
sqlite3_bind_text(stmt, 5, event_type, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 6, content, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 7, sig, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 8, tags_json, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 9, event_json, -1, SQLITE_TRANSIENT);
|
|
|
|
int step_rc = sqlite3_step(stmt);
|
|
int extended_errcode = sqlite3_extended_errcode(db);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (step_rc == SQLITE_DONE) {
|
|
(void)sqlite_db_insert_event_tags_json_with_db(db, id, tags_json);
|
|
}
|
|
|
|
if (out_step_rc) {
|
|
*out_step_rc = step_rc;
|
|
}
|
|
if (out_extended_errcode) {
|
|
*out_extended_errcode = extended_errcode;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int sqlite_db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_created_at) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !out_min_created_at || !out_max_created_at) return -1;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "SELECT MIN(created_at), MAX(created_at) FROM events";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
return -1;
|
|
}
|
|
|
|
long long min_ts = 0;
|
|
long long max_ts = 0;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
min_ts = sqlite3_column_int64(stmt, 0);
|
|
max_ts = sqlite3_column_int64(stmt, 1);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
*out_min_created_at = min_ts;
|
|
*out_max_created_at = max_ts;
|
|
return 0;
|
|
}
|
|
|
|
int sqlite_db_event_id_exists(const char* event_id, int* out_exists) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !event_id || !out_exists) return -1;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "SELECT 1 FROM events WHERE id=? LIMIT 1";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_text(stmt, 1, event_id, 64, SQLITE_STATIC);
|
|
int exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0;
|
|
sqlite3_finalize(stmt);
|
|
|
|
*out_exists = exists;
|
|
return 0;
|
|
}
|
|
|
|
cJSON* sqlite_db_retrieve_event_by_id(const char* event_id) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !event_id) return NULL;
|
|
|
|
const char* sql =
|
|
"SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE id = ?";
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
return NULL;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC);
|
|
|
|
cJSON* event = NULL;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
event = cJSON_CreateObject();
|
|
if (event) {
|
|
cJSON_AddStringToObject(event, "id", (const char*)sqlite3_column_text(stmt, 0));
|
|
cJSON_AddStringToObject(event, "pubkey", (const char*)sqlite3_column_text(stmt, 1));
|
|
cJSON_AddNumberToObject(event, "created_at", sqlite3_column_int64(stmt, 2));
|
|
cJSON_AddNumberToObject(event, "kind", sqlite3_column_int(stmt, 3));
|
|
cJSON_AddStringToObject(event, "content", (const char*)sqlite3_column_text(stmt, 4));
|
|
cJSON_AddStringToObject(event, "sig", (const char*)sqlite3_column_text(stmt, 5));
|
|
|
|
const char* tags_json = (const char*)sqlite3_column_text(stmt, 6);
|
|
if (tags_json) {
|
|
cJSON* tags = cJSON_Parse(tags_json);
|
|
if (tags) {
|
|
cJSON_AddItemToObject(event, "tags", tags);
|
|
} else {
|
|
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
|
|
}
|
|
} else {
|
|
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
|
|
}
|
|
}
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return event;
|
|
}
|
|
|
|
char* sqlite_db_get_latest_event_pubkey_for_kind_dup(int kind) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db) return NULL;
|
|
|
|
const char* sql = "SELECT pubkey FROM events WHERE kind = ? ORDER BY created_at DESC LIMIT 1";
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
return NULL;
|
|
}
|
|
|
|
sqlite3_bind_int(stmt, 1, kind);
|
|
|
|
char* pubkey_dup = NULL;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char* pubkey = (const char*)sqlite3_column_text(stmt, 0);
|
|
if (pubkey) {
|
|
pubkey_dup = strdup(pubkey);
|
|
}
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return pubkey_dup;
|
|
}
|
|
|
|
int sqlite_db_get_config_row_count(int* out_count) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !out_count) return -1;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "SELECT COUNT(*) FROM config";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
return -1;
|
|
}
|
|
|
|
int count = 0;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
count = sqlite3_column_int(stmt, 0);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
*out_count = count;
|
|
return 0;
|
|
}
|
|
|
|
int sqlite_db_store_event_tags_cjson(const char* event_id, const cJSON* tags) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !event_id || !tags || !cJSON_IsArray(tags)) {
|
|
return 0; // Not an error if no tags
|
|
}
|
|
|
|
const char* sql = "INSERT INTO event_tags (event_id, tag_name, tag_value, tag_index) VALUES (?, ?, ?, ?)";
|
|
sqlite3_stmt* stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
DEBUG_ERROR("Failed to prepare event_tags insert: %s", sqlite3_errmsg(db));
|
|
return -1;
|
|
}
|
|
|
|
int tag_index = 0;
|
|
cJSON* tag = NULL;
|
|
cJSON_ArrayForEach(tag, tags) {
|
|
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
|
cJSON* name = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* value = cJSON_GetArrayItem(tag, 1);
|
|
|
|
if (cJSON_IsString(name) && cJSON_IsString(value)) {
|
|
sqlite3_reset(stmt);
|
|
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC);
|
|
sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(name), -1, SQLITE_STATIC);
|
|
sqlite3_bind_text(stmt, 3, cJSON_GetStringValue(value), -1, SQLITE_STATIC);
|
|
sqlite3_bind_int(stmt, 4, tag_index);
|
|
|
|
rc = sqlite3_step(stmt);
|
|
if (rc != SQLITE_DONE) {
|
|
DEBUG_ERROR("Failed to insert event tag: %s", sqlite3_errmsg(db));
|
|
}
|
|
}
|
|
}
|
|
tag_index++;
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return 0;
|
|
}
|
|
|
|
int sqlite_db_populate_event_tags_from_existing(void) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db) return -1;
|
|
|
|
sqlite3_stmt* check_stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM event_tags", -1, &check_stmt, NULL) != SQLITE_OK) {
|
|
return -1;
|
|
}
|
|
|
|
if (sqlite3_step(check_stmt) == SQLITE_ROW && sqlite3_column_int(check_stmt, 0) > 0) {
|
|
sqlite3_finalize(check_stmt);
|
|
DEBUG_INFO("event_tags already populated, skipping");
|
|
return 0;
|
|
}
|
|
sqlite3_finalize(check_stmt);
|
|
|
|
DEBUG_INFO("Populating event_tags from existing events...");
|
|
|
|
const char* sql = "SELECT id, tags FROM events WHERE tags != '[]'";
|
|
sqlite3_stmt* stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) return -1;
|
|
|
|
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, NULL);
|
|
|
|
int event_count = 0;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char* event_id = (const char*)sqlite3_column_text(stmt, 0);
|
|
const char* tags_json = (const char*)sqlite3_column_text(stmt, 1);
|
|
|
|
if (event_id && tags_json) {
|
|
cJSON* tags = cJSON_Parse(tags_json);
|
|
if (tags) {
|
|
sqlite_db_store_event_tags_cjson(event_id, tags);
|
|
cJSON_Delete(tags);
|
|
event_count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
|
|
|
|
DEBUG_INFO("Populated event_tags for %d events", event_count);
|
|
return 0;
|
|
}
|
|
|
|
int sqlite_db_add_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !rule_type || !pattern_type || !pattern_value) return -1;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value) VALUES (?, ?, ?)";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_TRANSIENT);
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
|
}
|
|
|
|
int sqlite_db_remove_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !rule_type || !pattern_type || !pattern_value) return -1;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
const char* sql = "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?";
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_TRANSIENT);
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
|
}
|
|
|
|
int sqlite_db_delete_wot_whitelist_rules(void) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db) return -1;
|
|
|
|
const char* sql = "DELETE FROM auth_rules WHERE rule_type = 'wot_whitelist'";
|
|
int rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
|
|
return (rc == SQLITE_OK) ? 0 : -1;
|
|
}
|
|
|
|
int sqlite_db_count_wot_whitelist_rules(void) {
|
|
if (!sqlite_db_is_available()) return 0;
|
|
|
|
const char* sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'wot_whitelist' AND active = 1";
|
|
int count = 0;
|
|
if (sqlite_db_count_with_sql(sql, NULL, 0, &count) != 0) {
|
|
return 0;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
int sqlite_db_table_exists(const char* table_name, int* out_exists) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !table_name || !out_exists) return -1;
|
|
|
|
const char* sql = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1";
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, table_name, -1, SQLITE_TRANSIENT);
|
|
*out_exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0;
|
|
sqlite3_finalize(stmt);
|
|
return 0;
|
|
}
|
|
|
|
char* sqlite_db_get_schema_version_dup(void) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db) return NULL;
|
|
|
|
const char* sql = "SELECT value FROM schema_info WHERE key = 'version'";
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
return NULL;
|
|
}
|
|
|
|
char* result = NULL;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char* version = (const char*)sqlite3_column_text(stmt, 0);
|
|
if (version) {
|
|
result = strdup(version);
|
|
}
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return result;
|
|
}
|
|
|
|
int sqlite_db_exec_sql(const char* sql) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db || !sql) return -1;
|
|
|
|
int rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
|
|
return (rc == SQLITE_OK) ? 0 : -1;
|
|
}
|
|
|
|
int sqlite_db_wal_checkpoint_passive(void) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db) return -1;
|
|
|
|
int rc = sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_PASSIVE, NULL, NULL);
|
|
return (rc == SQLITE_OK) ? 0 : -1;
|
|
}
|
|
|
|
int sqlite_db_wal_checkpoint_truncate(void) {
|
|
sqlite3* db = sqlite_db_active_connection();
|
|
if (!db) return -1;
|
|
|
|
int rc = sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_TRUNCATE, NULL, NULL);
|
|
return (rc == SQLITE_OK) ? 0 : -1;
|
|
}
|