Files
nostr_terminal/src/db.c
T

710 lines
20 KiB
C

#include "db.h"
#include <errno.h>
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
static sqlite3 *g_db = NULL;
static char g_local_value[8192];
static int db_bind_text_or_null(sqlite3_stmt *stmt, int index, const char *value) {
if (value == NULL) {
return sqlite3_bind_null(stmt, index);
}
return sqlite3_bind_text(stmt, index, value, -1, SQLITE_TRANSIENT);
}
static int db_exec_sql(const char *sql) {
char *errmsg = NULL;
int rc;
if (g_db == NULL) {
fprintf(stderr, "db_exec_sql: database is not open\n");
return -1;
}
rc = sqlite3_exec(g_db, sql, NULL, NULL, &errmsg);
if (rc != SQLITE_OK) {
fprintf(stderr, "sqlite exec error: %s\n", errmsg ? errmsg : "unknown error");
sqlite3_free(errmsg);
return -1;
}
return 0;
}
static int is_regular_kind(int kind) {
if (kind == 1 || kind == 2) {
return 1;
}
if (kind >= 4 && kind <= 44) {
return 1;
}
if (kind >= 1000 && kind <= 9999) {
return 1;
}
return 0;
}
static int is_replaceable_kind(int kind) {
if (kind == 0 || kind == 3) {
return 1;
}
if (kind >= 10000 && kind <= 19999) {
return 1;
}
return 0;
}
static int is_addressable_kind(int kind) {
return (kind >= 30000 && kind <= 39999);
}
static int db_get_relay_urls_filtered(const char *where_clause, char ***urls, int *count) {
sqlite3_stmt *stmt = NULL;
int rc;
int n = 0;
int i = 0;
char sql_count[256];
char sql_select[256];
char **out_urls = NULL;
if (urls == NULL || count == NULL) {
fprintf(stderr, "db_get_relay_urls_filtered: invalid arguments\n");
return -1;
}
*urls = NULL;
*count = 0;
if (g_db == NULL) {
fprintf(stderr, "db_get_relay_urls_filtered: database is not open\n");
return -1;
}
snprintf(sql_count, sizeof(sql_count), "SELECT COUNT(*) FROM relays WHERE %s;", where_clause);
rc = sqlite3_prepare_v2(g_db, sql_count, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "sqlite prepare error: %s\n", sqlite3_errmsg(g_db));
return -1;
}
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
n = sqlite3_column_int(stmt, 0);
} else {
fprintf(stderr, "sqlite step error: %s\n", sqlite3_errmsg(g_db));
sqlite3_finalize(stmt);
return -1;
}
sqlite3_finalize(stmt);
stmt = NULL;
if (n <= 0) {
*count = 0;
return 0;
}
out_urls = (char **)calloc((size_t)n, sizeof(char *));
if (out_urls == NULL) {
fprintf(stderr, "memory allocation failed\n");
return -1;
}
snprintf(sql_select, sizeof(sql_select), "SELECT url FROM relays WHERE %s ORDER BY url;", where_clause);
rc = sqlite3_prepare_v2(g_db, sql_select, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "sqlite prepare error: %s\n", sqlite3_errmsg(g_db));
free(out_urls);
return -1;
}
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
const unsigned char *url = sqlite3_column_text(stmt, 0);
if (i >= n) {
break;
}
out_urls[i] = strdup((const char *)(url ? url : (const unsigned char *)""));
if (out_urls[i] == NULL) {
fprintf(stderr, "memory allocation failed\n");
sqlite3_finalize(stmt);
for (int j = 0; j < i; j++) {
free(out_urls[j]);
}
free(out_urls);
return -1;
}
i++;
}
if (rc != SQLITE_DONE) {
fprintf(stderr, "sqlite step error: %s\n", sqlite3_errmsg(g_db));
sqlite3_finalize(stmt);
for (int j = 0; j < i; j++) {
free(out_urls[j]);
}
free(out_urls);
return -1;
}
sqlite3_finalize(stmt);
*urls = out_urls;
*count = i;
return 0;
}
int db_open(void) {
const char *home = getenv("HOME");
char dir_path[4096];
char db_path[4096];
int rc;
if (g_db != NULL) {
return 0;
}
if (home == NULL || home[0] == '\0') {
fprintf(stderr, "db_open: HOME is not set\n");
return -1;
}
snprintf(dir_path, sizeof(dir_path), "%s/.nostr", home);
if (mkdir(dir_path, 0700) != 0 && errno != EEXIST) {
fprintf(stderr, "db_open: failed to create directory '%s': %s\n", dir_path, strerror(errno));
return -1;
}
if (snprintf(db_path, sizeof(db_path), "%s/nostr.db", dir_path) >= (int)sizeof(db_path)) {
fprintf(stderr, "db_open: database path too long\n");
return -1;
}
rc = sqlite3_open(db_path, &g_db);
if (rc != SQLITE_OK) {
fprintf(stderr, "db_open: sqlite open failed: %s\n", sqlite3_errmsg(g_db));
if (g_db != NULL) {
sqlite3_close(g_db);
g_db = NULL;
}
return -1;
}
if (db_exec_sql("PRAGMA foreign_keys = ON;") != 0) {
db_close();
return -1;
}
return 0;
}
void db_close(void) {
if (g_db != NULL) {
sqlite3_close(g_db);
g_db = NULL;
}
}
int db_init_schema(void) {
const char *schema_sql =
"CREATE TABLE IF NOT EXISTS regular_events ("
"id TEXT PRIMARY KEY CHECK(LENGTH(id) = 64),"
"kind INTEGER,"
"pubkey TEXT CHECK(LENGTH(pubkey) = 64),"
"created_at INTEGER NOT NULL,"
"sig TEXT CHECK(LENGTH(sig) = 128),"
"content TEXT,"
"read INTEGER DEFAULT 0,"
"tags TEXT"
");"
"CREATE TABLE IF NOT EXISTS replaceable_events ("
"id TEXT PRIMARY KEY CHECK(LENGTH(id) = 64),"
"kind INTEGER,"
"pubkey TEXT CHECK(LENGTH(pubkey) = 64),"
"created_at INTEGER NOT NULL,"
"sig TEXT CHECK(LENGTH(sig) = 128),"
"content TEXT,"
"tags TEXT"
");"
"CREATE UNIQUE INDEX IF NOT EXISTS idx_replaceable_pubkey_kind ON replaceable_events (pubkey, kind);"
"CREATE TABLE IF NOT EXISTS addressable_events ("
"id TEXT PRIMARY KEY CHECK(LENGTH(id) = 64),"
"kind INTEGER,"
"pubkey TEXT CHECK(LENGTH(pubkey) = 64),"
"created_at INTEGER NOT NULL,"
"sig TEXT CHECK(LENGTH(sig) = 128),"
"content TEXT,"
"tags TEXT,"
"d TEXT NOT NULL"
");"
"CREATE UNIQUE INDEX IF NOT EXISTS idx_addressable_pubkey_kind_d ON addressable_events (pubkey, kind, d);"
"CREATE TABLE IF NOT EXISTS relays ("
"url TEXT PRIMARY KEY,"
"name TEXT,"
"description TEXT,"
"read INTEGER DEFAULT 0,"
"write INTEGER DEFAULT 0,"
"supported_nips TEXT,"
"software TEXT,"
"version TEXT"
");"
"CREATE TABLE IF NOT EXISTS relay_posts ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"relay_url TEXT,"
"event_id TEXT CHECK(LENGTH(event_id) = 64),"
"timestamp INTEGER,"
"posted INTEGER,"
"response TEXT"
");"
"CREATE TABLE IF NOT EXISTS people ("
"pubkey TEXT PRIMARY KEY,"
"name TEXT,"
"following INTEGER DEFAULT 0"
");"
"CREATE TABLE IF NOT EXISTS local ("
"key TEXT PRIMARY KEY,"
"value TEXT"
");";
return db_exec_sql(schema_sql);
}
int db_store_event(const char *id,
int kind,
const char *pubkey,
int created_at,
const char *sig,
const char *content,
const char *tags_json,
const char *d_tag) {
sqlite3_stmt *stmt = NULL;
int rc;
const char *sql = NULL;
if (g_db == NULL) {
fprintf(stderr, "db_store_event: database is not open\n");
return -1;
}
if (id == NULL || pubkey == NULL || sig == NULL) {
fprintf(stderr, "db_store_event: id/pubkey/sig are required\n");
return -1;
}
if (is_regular_kind(kind)) {
sql =
"INSERT OR IGNORE INTO regular_events "
"(id, kind, pubkey, created_at, sig, content, tags) "
"VALUES (?, ?, ?, ?, ?, ?, ?);";
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "sqlite prepare error: %s\n", sqlite3_errmsg(g_db));
return -1;
}
sqlite3_bind_text(stmt, 1, id, -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 2, kind);
sqlite3_bind_text(stmt, 3, pubkey, -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 4, created_at);
sqlite3_bind_text(stmt, 5, sig, -1, SQLITE_TRANSIENT);
db_bind_text_or_null(stmt, 6, content);
db_bind_text_or_null(stmt, 7, tags_json);
} else if (is_replaceable_kind(kind)) {
sql =
"INSERT INTO replaceable_events "
"(id, kind, pubkey, created_at, sig, content, tags) "
"VALUES (?, ?, ?, ?, ?, ?, ?) "
"ON CONFLICT(pubkey, kind) DO UPDATE SET "
"id = excluded.id, "
"created_at = excluded.created_at, "
"sig = excluded.sig, "
"content = excluded.content, "
"tags = excluded.tags "
"WHERE excluded.created_at > replaceable_events.created_at;";
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "sqlite prepare error: %s\n", sqlite3_errmsg(g_db));
return -1;
}
sqlite3_bind_text(stmt, 1, id, -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 2, kind);
sqlite3_bind_text(stmt, 3, pubkey, -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 4, created_at);
sqlite3_bind_text(stmt, 5, sig, -1, SQLITE_TRANSIENT);
db_bind_text_or_null(stmt, 6, content);
db_bind_text_or_null(stmt, 7, tags_json);
} else if (is_addressable_kind(kind)) {
if (d_tag == NULL || d_tag[0] == '\0') {
fprintf(stderr, "db_store_event: d_tag is required for addressable kinds\n");
return -1;
}
sql =
"INSERT INTO addressable_events "
"(id, kind, pubkey, created_at, sig, content, tags, d) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?) "
"ON CONFLICT(pubkey, kind, d) DO UPDATE SET "
"id = excluded.id, "
"created_at = excluded.created_at, "
"sig = excluded.sig, "
"content = excluded.content, "
"tags = excluded.tags "
"WHERE excluded.created_at > addressable_events.created_at;";
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "sqlite prepare error: %s\n", sqlite3_errmsg(g_db));
return -1;
}
sqlite3_bind_text(stmt, 1, id, -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 2, kind);
sqlite3_bind_text(stmt, 3, pubkey, -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 4, created_at);
sqlite3_bind_text(stmt, 5, sig, -1, SQLITE_TRANSIENT);
db_bind_text_or_null(stmt, 6, content);
db_bind_text_or_null(stmt, 7, tags_json);
sqlite3_bind_text(stmt, 8, d_tag, -1, SQLITE_TRANSIENT);
} else {
fprintf(stderr, "db_store_event: unsupported kind %d\n", kind);
return -1;
}
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
fprintf(stderr, "sqlite step error: %s\n", sqlite3_errmsg(g_db));
sqlite3_finalize(stmt);
return -1;
}
sqlite3_finalize(stmt);
return 0;
}
int db_store_relay(const char *url, int read, int write) {
sqlite3_stmt *stmt = NULL;
int rc;
const char *sql =
"INSERT INTO relays (url, read, write) VALUES (?, ?, ?) "
"ON CONFLICT(url) DO UPDATE SET read = excluded.read, write = excluded.write;";
if (g_db == NULL) {
fprintf(stderr, "db_store_relay: database is not open\n");
return -1;
}
if (url == NULL || url[0] == '\0') {
fprintf(stderr, "db_store_relay: url is required\n");
return -1;
}
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "sqlite prepare error: %s\n", sqlite3_errmsg(g_db));
return -1;
}
sqlite3_bind_text(stmt, 1, url, -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 2, read ? 1 : 0);
sqlite3_bind_int(stmt, 3, write ? 1 : 0);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
fprintf(stderr, "sqlite step error: %s\n", sqlite3_errmsg(g_db));
sqlite3_finalize(stmt);
return -1;
}
sqlite3_finalize(stmt);
return 0;
}
int db_get_relays(char ***urls, int **read_flags, int **write_flags, int *count) {
sqlite3_stmt *stmt = NULL;
sqlite3_stmt *count_stmt = NULL;
int rc;
int n = 0;
int i = 0;
char **out_urls = NULL;
int *out_read = NULL;
int *out_write = NULL;
const char *sql_count = "SELECT COUNT(*) FROM relays;";
const char *sql_select = "SELECT url, read, write FROM relays ORDER BY url;";
if (urls == NULL || read_flags == NULL || write_flags == NULL || count == NULL) {
fprintf(stderr, "db_get_relays: invalid arguments\n");
return -1;
}
*urls = NULL;
*read_flags = NULL;
*write_flags = NULL;
*count = 0;
if (g_db == NULL) {
fprintf(stderr, "db_get_relays: database is not open\n");
return -1;
}
rc = sqlite3_prepare_v2(g_db, sql_count, -1, &count_stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "sqlite prepare error: %s\n", sqlite3_errmsg(g_db));
return -1;
}
rc = sqlite3_step(count_stmt);
if (rc == SQLITE_ROW) {
n = sqlite3_column_int(count_stmt, 0);
} else {
fprintf(stderr, "sqlite step error: %s\n", sqlite3_errmsg(g_db));
sqlite3_finalize(count_stmt);
return -1;
}
sqlite3_finalize(count_stmt);
count_stmt = NULL;
if (n <= 0) {
return 0;
}
out_urls = (char **)calloc((size_t)n, sizeof(char *));
out_read = (int *)calloc((size_t)n, sizeof(int));
out_write = (int *)calloc((size_t)n, sizeof(int));
if (out_urls == NULL || out_read == NULL || out_write == NULL) {
fprintf(stderr, "memory allocation failed\n");
free(out_urls);
free(out_read);
free(out_write);
return -1;
}
rc = sqlite3_prepare_v2(g_db, sql_select, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "sqlite prepare error: %s\n", sqlite3_errmsg(g_db));
free(out_urls);
free(out_read);
free(out_write);
return -1;
}
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
const unsigned char *url = sqlite3_column_text(stmt, 0);
if (i >= n) {
break;
}
out_urls[i] = strdup((const char *)(url ? url : (const unsigned char *)""));
if (out_urls[i] == NULL) {
fprintf(stderr, "memory allocation failed\n");
sqlite3_finalize(stmt);
for (int j = 0; j < i; j++) {
free(out_urls[j]);
}
free(out_urls);
free(out_read);
free(out_write);
return -1;
}
out_read[i] = sqlite3_column_int(stmt, 1);
out_write[i] = sqlite3_column_int(stmt, 2);
i++;
}
if (rc != SQLITE_DONE) {
fprintf(stderr, "sqlite step error: %s\n", sqlite3_errmsg(g_db));
sqlite3_finalize(stmt);
for (int j = 0; j < i; j++) {
free(out_urls[j]);
}
free(out_urls);
free(out_read);
free(out_write);
return -1;
}
sqlite3_finalize(stmt);
*urls = out_urls;
*read_flags = out_read;
*write_flags = out_write;
*count = i;
return 0;
}
int db_get_read_relays(char ***urls, int *count) {
return db_get_relay_urls_filtered("read = 1", urls, count);
}
int db_get_write_relays(char ***urls, int *count) {
return db_get_relay_urls_filtered("write = 1", urls, count);
}
int db_store_person(const char *pubkey, const char *name, int following) {
sqlite3_stmt *stmt = NULL;
int rc;
const char *sql =
"INSERT INTO people (pubkey, name, following) VALUES (?, ?, ?) "
"ON CONFLICT(pubkey) DO UPDATE SET name = excluded.name, following = excluded.following;";
if (g_db == NULL) {
fprintf(stderr, "db_store_person: database is not open\n");
return -1;
}
if (pubkey == NULL || pubkey[0] == '\0') {
fprintf(stderr, "db_store_person: pubkey is required\n");
return -1;
}
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "sqlite prepare error: %s\n", sqlite3_errmsg(g_db));
return -1;
}
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_TRANSIENT);
db_bind_text_or_null(stmt, 2, name);
sqlite3_bind_int(stmt, 3, following ? 1 : 0);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
fprintf(stderr, "sqlite step error: %s\n", sqlite3_errmsg(g_db));
sqlite3_finalize(stmt);
return -1;
}
sqlite3_finalize(stmt);
return 0;
}
int db_log_relay_post(const char *relay_url, const char *event_id, int posted, const char *response) {
sqlite3_stmt *stmt = NULL;
int rc;
time_t now = time(NULL);
const char *sql =
"INSERT INTO relay_posts (relay_url, event_id, timestamp, posted, response) "
"VALUES (?, ?, ?, ?, ?);";
if (g_db == NULL) {
fprintf(stderr, "db_log_relay_post: database is not open\n");
return -1;
}
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "sqlite prepare error: %s\n", sqlite3_errmsg(g_db));
return -1;
}
db_bind_text_or_null(stmt, 1, relay_url);
db_bind_text_or_null(stmt, 2, event_id);
sqlite3_bind_int(stmt, 3, (int)now);
sqlite3_bind_int(stmt, 4, posted ? 1 : 0);
db_bind_text_or_null(stmt, 5, response);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
fprintf(stderr, "sqlite step error: %s\n", sqlite3_errmsg(g_db));
sqlite3_finalize(stmt);
return -1;
}
sqlite3_finalize(stmt);
return 0;
}
int db_set_local(const char *key, const char *value) {
sqlite3_stmt *stmt = NULL;
int rc;
const char *sql =
"INSERT INTO local (key, value) VALUES (?, ?) "
"ON CONFLICT(key) DO UPDATE SET value = excluded.value;";
if (g_db == NULL) {
fprintf(stderr, "db_set_local: database is not open\n");
return -1;
}
if (key == NULL || key[0] == '\0') {
fprintf(stderr, "db_set_local: key is required\n");
return -1;
}
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "sqlite prepare error: %s\n", sqlite3_errmsg(g_db));
return -1;
}
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT);
db_bind_text_or_null(stmt, 2, value);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
fprintf(stderr, "sqlite step error: %s\n", sqlite3_errmsg(g_db));
sqlite3_finalize(stmt);
return -1;
}
sqlite3_finalize(stmt);
return 0;
}
const char *db_get_local(const char *key) {
sqlite3_stmt *stmt = NULL;
int rc;
const unsigned char *value;
const char *sql = "SELECT value FROM local WHERE key = ? LIMIT 1;";
if (g_db == NULL) {
fprintf(stderr, "db_get_local: database is not open\n");
return NULL;
}
if (key == NULL || key[0] == '\0') {
fprintf(stderr, "db_get_local: key is required\n");
return NULL;
}
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "sqlite prepare error: %s\n", sqlite3_errmsg(g_db));
return NULL;
}
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
value = sqlite3_column_text(stmt, 0);
if (value == NULL) {
sqlite3_finalize(stmt);
return NULL;
}
snprintf(g_local_value, sizeof(g_local_value), "%s", (const char *)value);
sqlite3_finalize(stmt);
return g_local_value;
}
if (rc != SQLITE_DONE) {
fprintf(stderr, "sqlite step error: %s\n", sqlite3_errmsg(g_db));
}
sqlite3_finalize(stmt);
return NULL;
}