617 lines
20 KiB
C
617 lines
20 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "nostr_block_list.h"
|
|
|
|
#include <pthread.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "nostr_handler.h"
|
|
#include "debug.h"
|
|
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
|
|
|
/* Optional process-lifetime signer for NIP-44 encrypt/decrypt of private
|
|
* block-list content. Owned by main.c; non-owning here. */
|
|
static nostr_signer_t* g_block_signer = NULL;
|
|
|
|
typedef struct {
|
|
didactyl_config_t* cfg;
|
|
int initialized;
|
|
int loaded;
|
|
pthread_mutex_t mutex;
|
|
|
|
char** pub_pubkeys;
|
|
int pub_pubkey_count;
|
|
char** pub_event_ids;
|
|
int pub_event_id_count;
|
|
char** pub_hashtags;
|
|
int pub_hashtag_count;
|
|
|
|
char** priv_pubkeys;
|
|
int priv_pubkey_count;
|
|
char** priv_event_ids;
|
|
int priv_event_id_count;
|
|
char** priv_hashtags;
|
|
int priv_hashtag_count;
|
|
} block_list_state_t;
|
|
|
|
static block_list_state_t g_block = {
|
|
.mutex = PTHREAD_MUTEX_INITIALIZER
|
|
};
|
|
|
|
static int is_hex_len_local(const char* s, size_t len) {
|
|
if (!s || strlen(s) != len) return 0;
|
|
for (size_t i = 0; i < len; i++) {
|
|
char c = s[i];
|
|
if (!((c >= '0' && c <= '9') ||
|
|
(c >= 'a' && c <= 'f') ||
|
|
(c >= 'A' && c <= 'F'))) {
|
|
return 0;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int str_array_contains(char** arr, int count, const char* s) {
|
|
if (!arr || count <= 0 || !s) return 0;
|
|
for (int i = 0; i < count; i++) {
|
|
if (arr[i] && strcmp(arr[i], s) == 0) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int str_array_add_unique(char*** arr, int* count, const char* s) {
|
|
if (!arr || !count || !s || s[0] == '\0') return -1;
|
|
if (str_array_contains(*arr, *count, s)) return 0;
|
|
|
|
char** grown = (char**)realloc(*arr, (size_t)(*count + 1) * sizeof(char*));
|
|
if (!grown) return -1;
|
|
|
|
char* dup = strdup(s);
|
|
if (!dup) return -1;
|
|
|
|
grown[*count] = dup;
|
|
*arr = grown;
|
|
(*count)++;
|
|
return 1;
|
|
}
|
|
|
|
static void str_array_free(char** arr, int count) {
|
|
if (!arr) return;
|
|
for (int i = 0; i < count; i++) {
|
|
free(arr[i]);
|
|
}
|
|
free(arr);
|
|
}
|
|
|
|
static void block_clear_sets_locked(void) {
|
|
str_array_free(g_block.pub_pubkeys, g_block.pub_pubkey_count);
|
|
str_array_free(g_block.pub_event_ids, g_block.pub_event_id_count);
|
|
str_array_free(g_block.pub_hashtags, g_block.pub_hashtag_count);
|
|
str_array_free(g_block.priv_pubkeys, g_block.priv_pubkey_count);
|
|
str_array_free(g_block.priv_event_ids, g_block.priv_event_id_count);
|
|
str_array_free(g_block.priv_hashtags, g_block.priv_hashtag_count);
|
|
|
|
g_block.pub_pubkeys = NULL;
|
|
g_block.pub_pubkey_count = 0;
|
|
g_block.pub_event_ids = NULL;
|
|
g_block.pub_event_id_count = 0;
|
|
g_block.pub_hashtags = NULL;
|
|
g_block.pub_hashtag_count = 0;
|
|
g_block.priv_pubkeys = NULL;
|
|
g_block.priv_pubkey_count = 0;
|
|
g_block.priv_event_ids = NULL;
|
|
g_block.priv_event_id_count = 0;
|
|
g_block.priv_hashtags = NULL;
|
|
g_block.priv_hashtag_count = 0;
|
|
}
|
|
|
|
static int add_tuple_to_sets_locked(const char* key, const char* value, int is_public) {
|
|
if (!key || !value || value[0] == '\0') return -1;
|
|
|
|
if (strcmp(key, "p") == 0) {
|
|
if (!is_hex_len_local(value, 64U)) return -1;
|
|
return is_public
|
|
? str_array_add_unique(&g_block.pub_pubkeys, &g_block.pub_pubkey_count, value)
|
|
: str_array_add_unique(&g_block.priv_pubkeys, &g_block.priv_pubkey_count, value);
|
|
}
|
|
|
|
if (strcmp(key, "e") == 0) {
|
|
if (!is_hex_len_local(value, 64U)) return -1;
|
|
return is_public
|
|
? str_array_add_unique(&g_block.pub_event_ids, &g_block.pub_event_id_count, value)
|
|
: str_array_add_unique(&g_block.priv_event_ids, &g_block.priv_event_id_count, value);
|
|
}
|
|
|
|
if (strcmp(key, "t") == 0) {
|
|
return is_public
|
|
? str_array_add_unique(&g_block.pub_hashtags, &g_block.pub_hashtag_count, value)
|
|
: str_array_add_unique(&g_block.priv_hashtags, &g_block.priv_hashtag_count, value);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void parse_tags_into_sets_locked(cJSON* tags, int is_public) {
|
|
if (!tags || !cJSON_IsArray(tags)) return;
|
|
|
|
cJSON* tag = NULL;
|
|
cJSON_ArrayForEach(tag, tags) {
|
|
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
|
|
cJSON* k = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* v = cJSON_GetArrayItem(tag, 1);
|
|
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) continue;
|
|
(void)add_tuple_to_sets_locked(k->valuestring, v->valuestring, is_public);
|
|
}
|
|
}
|
|
|
|
static cJSON* fetch_latest_kind10000_event(void) {
|
|
if (!g_block.cfg) return NULL;
|
|
|
|
cJSON* filter = cJSON_CreateObject();
|
|
cJSON* kinds = cJSON_CreateArray();
|
|
cJSON* authors = cJSON_CreateArray();
|
|
if (!filter || !kinds || !authors) {
|
|
cJSON_Delete(filter);
|
|
cJSON_Delete(kinds);
|
|
cJSON_Delete(authors);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10000));
|
|
cJSON_AddItemToObject(filter, "kinds", kinds);
|
|
cJSON_AddItemToArray(authors, cJSON_CreateString(g_block.cfg->keys.public_key_hex));
|
|
cJSON_AddItemToObject(filter, "authors", authors);
|
|
cJSON_AddNumberToObject(filter, "limit", 1);
|
|
|
|
char* events_json = nostr_handler_query_json(filter, 8000);
|
|
cJSON_Delete(filter);
|
|
if (!events_json) return NULL;
|
|
|
|
cJSON* arr = cJSON_Parse(events_json);
|
|
free(events_json);
|
|
if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) {
|
|
cJSON_Delete(arr);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON* ev0 = cJSON_GetArrayItem(arr, 0);
|
|
cJSON* out = ev0 ? cJSON_Duplicate(ev0, 1) : NULL;
|
|
cJSON_Delete(arr);
|
|
return out;
|
|
}
|
|
|
|
static int decrypt_private_tags_array(const char* encrypted_content, cJSON** out_array) {
|
|
if (!out_array) return -1;
|
|
*out_array = NULL;
|
|
|
|
if (!encrypted_content || encrypted_content[0] == '\0') {
|
|
*out_array = cJSON_CreateArray();
|
|
return *out_array ? 0 : -1;
|
|
}
|
|
|
|
/* Phase 3 item 13: route NIP-44 decrypt through the process signer when
|
|
* available so remote signer modes keep the nsec out of the agent. The
|
|
* signer verb returns a heap string via out-param; free it after parse.
|
|
* Falls back to the legacy raw-key path when no signer is set. */
|
|
const char* plain_src = NULL;
|
|
char* plain_to_free = NULL;
|
|
char* plaintext = NULL;
|
|
|
|
if (g_block_signer) {
|
|
int rc = nostr_signer_nip44_decrypt(g_block_signer,
|
|
g_block.cfg->keys.public_key_hex,
|
|
encrypted_content,
|
|
&plaintext);
|
|
if (rc != NOSTR_SUCCESS || !plaintext) {
|
|
return -1;
|
|
}
|
|
plain_src = plaintext;
|
|
plain_to_free = plaintext;
|
|
} else {
|
|
size_t cap = strlen(encrypted_content) + 4096;
|
|
plaintext = (char*)calloc(1, cap);
|
|
if (!plaintext) return -1;
|
|
int rc = nostr_nip44_decrypt(g_block.cfg->keys.private_key,
|
|
g_block.cfg->keys.public_key,
|
|
encrypted_content,
|
|
plaintext,
|
|
cap);
|
|
if (rc != 0) {
|
|
free(plaintext);
|
|
return -1;
|
|
}
|
|
plain_src = plaintext;
|
|
plain_to_free = plaintext;
|
|
}
|
|
|
|
cJSON* arr = cJSON_Parse(plain_src);
|
|
free(plain_to_free);
|
|
if (!arr || !cJSON_IsArray(arr)) {
|
|
cJSON_Delete(arr);
|
|
return -1;
|
|
}
|
|
|
|
*out_array = arr;
|
|
return 0;
|
|
}
|
|
|
|
static int encrypt_private_tags_array(cJSON* private_tags_array, char** out_content) {
|
|
if (!private_tags_array || !out_content || !cJSON_IsArray(private_tags_array)) return -1;
|
|
*out_content = NULL;
|
|
|
|
char* plain = cJSON_PrintUnformatted(private_tags_array);
|
|
if (!plain) return -1;
|
|
|
|
/* Phase 3 item 13: route NIP-44 encrypt through the process signer when
|
|
* available. The signer verb returns a heap string via out-param. Falls
|
|
* back to the legacy raw-key path when no signer is set. */
|
|
char* cipher = NULL;
|
|
if (g_block_signer) {
|
|
int rc = nostr_signer_nip44_encrypt(g_block_signer,
|
|
g_block.cfg->keys.public_key_hex,
|
|
plain,
|
|
&cipher);
|
|
free(plain);
|
|
if (rc != NOSTR_SUCCESS || !cipher) {
|
|
return -1;
|
|
}
|
|
*out_content = cipher;
|
|
return 0;
|
|
}
|
|
|
|
size_t cap = strlen(plain) * 4 + 4096;
|
|
cipher = (char*)calloc(1, cap);
|
|
if (!cipher) {
|
|
free(plain);
|
|
return -1;
|
|
}
|
|
|
|
int rc = nostr_nip44_encrypt(g_block.cfg->keys.private_key,
|
|
g_block.cfg->keys.public_key,
|
|
plain,
|
|
cipher,
|
|
cap);
|
|
free(plain);
|
|
if (rc != 0) {
|
|
free(cipher);
|
|
return -1;
|
|
}
|
|
|
|
*out_content = cipher;
|
|
return 0;
|
|
}
|
|
|
|
static int normalize_to_tuple(cJSON* tuple, char** out_k, char** out_v) {
|
|
if (!tuple || !out_k || !out_v || !cJSON_IsArray(tuple) || cJSON_GetArraySize(tuple) < 2) return -1;
|
|
cJSON* k = cJSON_GetArrayItem(tuple, 0);
|
|
cJSON* v = cJSON_GetArrayItem(tuple, 1);
|
|
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) return -1;
|
|
*out_k = k->valuestring;
|
|
*out_v = v->valuestring;
|
|
return 0;
|
|
}
|
|
|
|
static int tuple_equal(cJSON* a, cJSON* b) {
|
|
char *ak = NULL, *av = NULL, *bk = NULL, *bv = NULL;
|
|
if (normalize_to_tuple(a, &ak, &av) != 0) return 0;
|
|
if (normalize_to_tuple(b, &bk, &bv) != 0) return 0;
|
|
return strcmp(ak, bk) == 0 && strcmp(av, bv) == 0;
|
|
}
|
|
|
|
static int tags_contains_tuple(cJSON* tags, cJSON* tuple) {
|
|
if (!tags || !tuple || !cJSON_IsArray(tags) || !cJSON_IsArray(tuple)) return 0;
|
|
int n = cJSON_GetArraySize(tags);
|
|
for (int i = 0; i < n; i++) {
|
|
if (tuple_equal(cJSON_GetArrayItem(tags, i), tuple)) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int remove_tuple_all(cJSON* tags, cJSON* tuple) {
|
|
if (!tags || !tuple || !cJSON_IsArray(tags) || !cJSON_IsArray(tuple)) return 0;
|
|
int removed = 0;
|
|
for (int i = cJSON_GetArraySize(tags) - 1; i >= 0; i--) {
|
|
if (tuple_equal(cJSON_GetArrayItem(tags, i), tuple)) {
|
|
cJSON_DeleteItemFromArray(tags, i);
|
|
removed++;
|
|
}
|
|
}
|
|
return removed;
|
|
}
|
|
|
|
static cJSON* build_tuple(const char* tag_key, const char* tag_value) {
|
|
cJSON* tuple = cJSON_CreateArray();
|
|
if (!tuple) return NULL;
|
|
cJSON_AddItemToArray(tuple, cJSON_CreateString(tag_key ? tag_key : ""));
|
|
cJSON_AddItemToArray(tuple, cJSON_CreateString(tag_value ? tag_value : ""));
|
|
return tuple;
|
|
}
|
|
|
|
static int publish_kind10000(cJSON* public_tags, cJSON* private_tags, char* out_event_id64) {
|
|
if (!public_tags || !private_tags || !cJSON_IsArray(public_tags) || !cJSON_IsArray(private_tags)) return -1;
|
|
|
|
char* encrypted_content = NULL;
|
|
if (encrypt_private_tags_array(private_tags, &encrypted_content) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
nostr_publish_result_t result;
|
|
memset(&result, 0, sizeof(result));
|
|
int rc = nostr_handler_publish_kind_event(10000, encrypted_content, public_tags, &result);
|
|
free(encrypted_content);
|
|
if (rc != 0) {
|
|
nostr_handler_publish_result_free(&result);
|
|
return -1;
|
|
}
|
|
|
|
if (out_event_id64 && result.event_id[0] != '\0') {
|
|
snprintf(out_event_id64, 65, "%s", result.event_id);
|
|
}
|
|
|
|
nostr_handler_publish_result_free(&result);
|
|
return 0;
|
|
}
|
|
|
|
void nostr_block_list_set_signer(nostr_signer_t* signer) {
|
|
pthread_mutex_lock(&g_block.mutex);
|
|
g_block_signer = signer;
|
|
pthread_mutex_unlock(&g_block.mutex);
|
|
}
|
|
|
|
int nostr_block_list_init(didactyl_config_t* cfg) {
|
|
if (!cfg) return -1;
|
|
|
|
pthread_mutex_lock(&g_block.mutex);
|
|
g_block.cfg = cfg;
|
|
g_block.initialized = 1;
|
|
pthread_mutex_unlock(&g_block.mutex);
|
|
return 0;
|
|
}
|
|
|
|
int nostr_block_list_load_from_relays(void) {
|
|
if (!g_block.initialized || !g_block.cfg) return -1;
|
|
|
|
cJSON* ev = fetch_latest_kind10000_event();
|
|
|
|
pthread_mutex_lock(&g_block.mutex);
|
|
block_clear_sets_locked();
|
|
|
|
if (!ev) {
|
|
g_block.loaded = 1;
|
|
pthread_mutex_unlock(&g_block.mutex);
|
|
return 0;
|
|
}
|
|
|
|
cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags");
|
|
parse_tags_into_sets_locked(tags, 1);
|
|
|
|
cJSON* content = cJSON_GetObjectItemCaseSensitive(ev, "content");
|
|
if (content && cJSON_IsString(content) && content->valuestring && content->valuestring[0] != '\0') {
|
|
cJSON* private_tags = NULL;
|
|
if (decrypt_private_tags_array(content->valuestring, &private_tags) == 0) {
|
|
parse_tags_into_sets_locked(private_tags, 0);
|
|
cJSON_Delete(private_tags);
|
|
} else {
|
|
DEBUG_WARN("[didactyl] nostr_block_list: failed to decrypt kind 10000 private content");
|
|
}
|
|
}
|
|
|
|
g_block.loaded = 1;
|
|
pthread_mutex_unlock(&g_block.mutex);
|
|
cJSON_Delete(ev);
|
|
return 0;
|
|
}
|
|
|
|
int nostr_block_list_refresh(void) {
|
|
return nostr_block_list_load_from_relays();
|
|
}
|
|
|
|
void nostr_block_list_cleanup(void) {
|
|
pthread_mutex_lock(&g_block.mutex);
|
|
block_clear_sets_locked();
|
|
g_block.cfg = NULL;
|
|
g_block.loaded = 0;
|
|
g_block.initialized = 0;
|
|
g_block_signer = NULL;
|
|
pthread_mutex_unlock(&g_block.mutex);
|
|
}
|
|
|
|
int nostr_block_list_is_pubkey_blocked(const char* pubkey_hex) {
|
|
if (!pubkey_hex) return 0;
|
|
pthread_mutex_lock(&g_block.mutex);
|
|
int blocked = str_array_contains(g_block.pub_pubkeys, g_block.pub_pubkey_count, pubkey_hex) ||
|
|
str_array_contains(g_block.priv_pubkeys, g_block.priv_pubkey_count, pubkey_hex);
|
|
pthread_mutex_unlock(&g_block.mutex);
|
|
return blocked ? 1 : 0;
|
|
}
|
|
|
|
int nostr_block_list_is_event_blocked(const char* event_id_hex) {
|
|
if (!event_id_hex) return 0;
|
|
pthread_mutex_lock(&g_block.mutex);
|
|
int blocked = str_array_contains(g_block.pub_event_ids, g_block.pub_event_id_count, event_id_hex) ||
|
|
str_array_contains(g_block.priv_event_ids, g_block.priv_event_id_count, event_id_hex);
|
|
pthread_mutex_unlock(&g_block.mutex);
|
|
return blocked ? 1 : 0;
|
|
}
|
|
|
|
int nostr_block_list_is_hashtag_blocked(const char* hashtag) {
|
|
if (!hashtag) return 0;
|
|
pthread_mutex_lock(&g_block.mutex);
|
|
int blocked = str_array_contains(g_block.pub_hashtags, g_block.pub_hashtag_count, hashtag) ||
|
|
str_array_contains(g_block.priv_hashtags, g_block.priv_hashtag_count, hashtag);
|
|
pthread_mutex_unlock(&g_block.mutex);
|
|
return blocked ? 1 : 0;
|
|
}
|
|
|
|
int nostr_block_list_is_event_filtered(cJSON* event) {
|
|
if (!event || !cJSON_IsObject(event)) return 0;
|
|
|
|
cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id");
|
|
if (id && cJSON_IsString(id) && id->valuestring && nostr_block_list_is_event_blocked(id->valuestring)) {
|
|
return 1;
|
|
}
|
|
|
|
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
|
|
if (pubkey && cJSON_IsString(pubkey) && pubkey->valuestring && nostr_block_list_is_pubkey_blocked(pubkey->valuestring)) {
|
|
return 1;
|
|
}
|
|
|
|
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
|
|
if (tags && cJSON_IsArray(tags)) {
|
|
cJSON* tag = NULL;
|
|
cJSON_ArrayForEach(tag, tags) {
|
|
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
|
|
cJSON* k = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* v = cJSON_GetArrayItem(tag, 1);
|
|
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) continue;
|
|
if (strcmp(k->valuestring, "t") == 0 && nostr_block_list_is_hashtag_blocked(v->valuestring)) {
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int mutate_block_list(const char* tag_key, const char* tag_value, int add_op, int is_public) {
|
|
if (!g_block.initialized || !g_block.cfg || !tag_key || !tag_value) return -1;
|
|
if (!(strcmp(tag_key, "p") == 0 || strcmp(tag_key, "e") == 0 || strcmp(tag_key, "t") == 0)) return -1;
|
|
|
|
cJSON* ev = fetch_latest_kind10000_event();
|
|
cJSON* public_tags = cJSON_CreateArray();
|
|
cJSON* private_tags = cJSON_CreateArray();
|
|
if (!public_tags || !private_tags) {
|
|
cJSON_Delete(ev);
|
|
cJSON_Delete(public_tags);
|
|
cJSON_Delete(private_tags);
|
|
return -1;
|
|
}
|
|
|
|
if (ev && cJSON_IsObject(ev)) {
|
|
cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags");
|
|
if (tags && cJSON_IsArray(tags)) {
|
|
cJSON_Delete(public_tags);
|
|
public_tags = cJSON_Duplicate(tags, 1);
|
|
if (!public_tags) {
|
|
cJSON_Delete(ev);
|
|
cJSON_Delete(private_tags);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
cJSON* content = cJSON_GetObjectItemCaseSensitive(ev, "content");
|
|
if (content && cJSON_IsString(content) && content->valuestring && content->valuestring[0] != '\0') {
|
|
cJSON_Delete(private_tags);
|
|
if (decrypt_private_tags_array(content->valuestring, &private_tags) != 0) {
|
|
private_tags = cJSON_CreateArray();
|
|
}
|
|
if (!private_tags) {
|
|
cJSON_Delete(ev);
|
|
cJSON_Delete(public_tags);
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
cJSON* tuple = build_tuple(tag_key, tag_value);
|
|
if (!tuple) {
|
|
cJSON_Delete(ev);
|
|
cJSON_Delete(public_tags);
|
|
cJSON_Delete(private_tags);
|
|
return -1;
|
|
}
|
|
|
|
cJSON* target = is_public ? public_tags : private_tags;
|
|
cJSON* other = is_public ? private_tags : public_tags;
|
|
|
|
if (add_op) {
|
|
if (!tags_contains_tuple(target, tuple)) {
|
|
cJSON* dup = cJSON_Duplicate(tuple, 1);
|
|
if (!dup) {
|
|
cJSON_Delete(tuple);
|
|
cJSON_Delete(ev);
|
|
cJSON_Delete(public_tags);
|
|
cJSON_Delete(private_tags);
|
|
return -1;
|
|
}
|
|
cJSON_AddItemToArray(target, dup);
|
|
}
|
|
} else {
|
|
(void)remove_tuple_all(target, tuple);
|
|
(void)remove_tuple_all(other, tuple);
|
|
}
|
|
|
|
cJSON_Delete(tuple);
|
|
|
|
int rc = publish_kind10000(public_tags, private_tags, NULL);
|
|
cJSON_Delete(ev);
|
|
cJSON_Delete(public_tags);
|
|
cJSON_Delete(private_tags);
|
|
if (rc != 0) return -1;
|
|
|
|
return nostr_block_list_refresh();
|
|
}
|
|
|
|
int nostr_block_list_add(const char* tag_key, const char* tag_value, int is_public) {
|
|
return mutate_block_list(tag_key, tag_value, 1, is_public ? 1 : 0);
|
|
}
|
|
|
|
int nostr_block_list_remove(const char* tag_key, const char* tag_value) {
|
|
return mutate_block_list(tag_key, tag_value, 0, 0);
|
|
}
|
|
|
|
char* nostr_block_list_json(void) {
|
|
cJSON* root = cJSON_CreateObject();
|
|
cJSON* blocked_pubkeys = cJSON_CreateArray();
|
|
cJSON* blocked_events = cJSON_CreateArray();
|
|
cJSON* blocked_hashtags = cJSON_CreateArray();
|
|
if (!root || !blocked_pubkeys || !blocked_events || !blocked_hashtags) {
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(blocked_pubkeys);
|
|
cJSON_Delete(blocked_events);
|
|
cJSON_Delete(blocked_hashtags);
|
|
return NULL;
|
|
}
|
|
|
|
pthread_mutex_lock(&g_block.mutex);
|
|
|
|
for (int i = 0; i < g_block.pub_pubkey_count; i++) {
|
|
cJSON_AddItemToArray(blocked_pubkeys, cJSON_CreateString(g_block.pub_pubkeys[i]));
|
|
}
|
|
for (int i = 0; i < g_block.priv_pubkey_count; i++) {
|
|
cJSON_AddItemToArray(blocked_pubkeys, cJSON_CreateString(g_block.priv_pubkeys[i]));
|
|
}
|
|
|
|
for (int i = 0; i < g_block.pub_event_id_count; i++) {
|
|
cJSON_AddItemToArray(blocked_events, cJSON_CreateString(g_block.pub_event_ids[i]));
|
|
}
|
|
for (int i = 0; i < g_block.priv_event_id_count; i++) {
|
|
cJSON_AddItemToArray(blocked_events, cJSON_CreateString(g_block.priv_event_ids[i]));
|
|
}
|
|
|
|
for (int i = 0; i < g_block.pub_hashtag_count; i++) {
|
|
cJSON_AddItemToArray(blocked_hashtags, cJSON_CreateString(g_block.pub_hashtags[i]));
|
|
}
|
|
for (int i = 0; i < g_block.priv_hashtag_count; i++) {
|
|
cJSON_AddItemToArray(blocked_hashtags, cJSON_CreateString(g_block.priv_hashtags[i]));
|
|
}
|
|
|
|
int total = g_block.pub_pubkey_count + g_block.priv_pubkey_count +
|
|
g_block.pub_event_id_count + g_block.priv_event_id_count +
|
|
g_block.pub_hashtag_count + g_block.priv_hashtag_count;
|
|
|
|
cJSON_AddBoolToObject(root, "success", 1);
|
|
cJSON_AddBoolToObject(root, "loaded", g_block.loaded ? 1 : 0);
|
|
cJSON_AddItemToObject(root, "blocked_pubkeys", blocked_pubkeys);
|
|
cJSON_AddItemToObject(root, "blocked_events", blocked_events);
|
|
cJSON_AddItemToObject(root, "blocked_hashtags", blocked_hashtags);
|
|
cJSON_AddNumberToObject(root, "total_entries", total);
|
|
|
|
pthread_mutex_unlock(&g_block.mutex);
|
|
|
|
char* out = cJSON_PrintUnformatted(root);
|
|
cJSON_Delete(root);
|
|
return out;
|
|
}
|