588 lines
20 KiB
C
588 lines
20 KiB
C
/*
|
|
* bookmarks.c — NIP-44 encrypted Nostr bookmarks with directories
|
|
*
|
|
* Stores bookmarks as NIP-51 kind 30003 (bookmark sets) events, one per
|
|
* directory. Each directory's bookmarks are NIP-44 encrypted (self-to-self)
|
|
* and stored in the event's content field as a JSON tag array:
|
|
*
|
|
* [["bookmark", "https://example.com", "Example", 1690000000], ...]
|
|
*
|
|
* The encrypted event is published to the bootstrap relays and stored in
|
|
* the local SQLite database. On startup, the relay fetch retrieves kind
|
|
* 30003 events, which are decrypted and loaded into the in-memory list.
|
|
*/
|
|
|
|
#include "bookmarks.h"
|
|
#include "db.h"
|
|
#include "settings.h"
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
|
|
#include "nostr_core/nostr_core.h"
|
|
#include "../nostr_core_lib/cjson/cJSON.h"
|
|
|
|
/* ── Global state ──────────────────────────────────────────────────── */
|
|
|
|
static nostr_signer_t *g_signer = NULL;
|
|
static char g_pubkey[65] = {0};
|
|
static int g_have_signer = 0;
|
|
|
|
static bookmark_dir_t *g_dirs = NULL;
|
|
static int g_dir_count = 0;
|
|
static int g_dir_cap = 0;
|
|
|
|
/* ── Helpers ───────────────────────────────────────────────────────── */
|
|
|
|
/* Find a directory by name. Returns the index, or -1 if not found. */
|
|
static int find_dir(const char *name) {
|
|
if (name == NULL) return -1;
|
|
for (int i = 0; i < g_dir_count; i++) {
|
|
if (strcmp(g_dirs[i].name, name) == 0) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* Ensure a directory exists in the in-memory list. Returns the index. */
|
|
static int ensure_dir(const char *name) {
|
|
int idx = find_dir(name);
|
|
if (idx >= 0) return idx;
|
|
|
|
/* Grow the array if needed. */
|
|
if (g_dir_count >= g_dir_cap) {
|
|
int new_cap = g_dir_cap == 0 ? 8 : g_dir_cap * 2;
|
|
bookmark_dir_t *new_arr = g_realloc(g_dirs, new_cap * sizeof(*new_arr));
|
|
if (new_arr == NULL) return -1;
|
|
g_dirs = new_arr;
|
|
g_dir_cap = new_cap;
|
|
}
|
|
|
|
idx = g_dir_count++;
|
|
memset(&g_dirs[idx], 0, sizeof(g_dirs[idx]));
|
|
snprintf(g_dirs[idx].name, sizeof(g_dirs[idx].name), "%s", name);
|
|
return idx;
|
|
}
|
|
|
|
/* Free the bookmarks in a directory (but not the directory struct itself). */
|
|
static void free_dir_bookmarks(bookmark_dir_t *dir) {
|
|
if (dir->bookmarks == NULL) return;
|
|
for (int i = 0; i < dir->count; i++) {
|
|
g_free(dir->bookmarks[i].url);
|
|
g_free(dir->bookmarks[i].title);
|
|
}
|
|
g_free(dir->bookmarks);
|
|
dir->bookmarks = NULL;
|
|
dir->count = 0;
|
|
}
|
|
|
|
/* Find a bookmark in a directory by URL. Returns the index, or -1. */
|
|
static int find_bookmark_in_dir(const bookmark_dir_t *dir, const char *url) {
|
|
if (dir == NULL || url == NULL) return -1;
|
|
for (int i = 0; i < dir->count; i++) {
|
|
if (strcmp(dir->bookmarks[i].url, url) == 0) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* ── Serialization ─────────────────────────────────────────────────── */
|
|
|
|
/* Serialize a directory's bookmarks to NIP-51 tag-array JSON.
|
|
* Returns a newly allocated string (caller must g_free). */
|
|
static char *dir_to_json(const bookmark_dir_t *dir) {
|
|
cJSON *arr = cJSON_CreateArray();
|
|
for (int i = 0; i < dir->count; i++) {
|
|
cJSON *tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString("bookmark"));
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString(dir->bookmarks[i].url));
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString(dir->bookmarks[i].title));
|
|
cJSON_AddItemToArray(tag,
|
|
cJSON_CreateNumber(dir->bookmarks[i].added));
|
|
cJSON_AddItemToArray(arr, tag);
|
|
}
|
|
char *json = cJSON_PrintUnformatted(arr);
|
|
cJSON_Delete(arr);
|
|
return json;
|
|
}
|
|
|
|
/* Parse a NIP-51 tag-array JSON string into a directory's bookmarks.
|
|
* Replaces the directory's current bookmark list. */
|
|
static int dir_load_from_json(bookmark_dir_t *dir, const char *json) {
|
|
if (dir == NULL || json == NULL) return -1;
|
|
|
|
cJSON *arr = cJSON_Parse(json);
|
|
if (arr == NULL) return -1;
|
|
if (!cJSON_IsArray(arr)) {
|
|
cJSON_Delete(arr);
|
|
return -1;
|
|
}
|
|
|
|
/* Free existing bookmarks. */
|
|
free_dir_bookmarks(dir);
|
|
|
|
int count = cJSON_GetArraySize(arr);
|
|
if (count > BOOKMARKS_MAX_PER_DIR) count = BOOKMARKS_MAX_PER_DIR;
|
|
|
|
dir->bookmarks = g_new0(bookmark_t, count);
|
|
dir->count = 0;
|
|
|
|
cJSON *tag;
|
|
cJSON_ArrayForEach(tag, arr) {
|
|
if (dir->count >= count) break;
|
|
if (!cJSON_IsArray(tag)) continue;
|
|
|
|
cJSON *t0 = cJSON_GetArrayItem(tag, 0);
|
|
cJSON *t1 = cJSON_GetArrayItem(tag, 1);
|
|
cJSON *t2 = cJSON_GetArrayItem(tag, 2);
|
|
cJSON *t3 = cJSON_GetArrayItem(tag, 3);
|
|
|
|
if (!cJSON_IsString(t0) || strcmp(t0->valuestring, "bookmark") != 0)
|
|
continue;
|
|
if (!cJSON_IsString(t1)) continue;
|
|
|
|
dir->bookmarks[dir->count].url = g_strdup(t1->valuestring);
|
|
dir->bookmarks[dir->count].title = g_strdup(
|
|
(t2 && cJSON_IsString(t2)) ? t2->valuestring : "");
|
|
dir->bookmarks[dir->count].added =
|
|
(t3 && cJSON_IsNumber(t3)) ? (long)t3->valuedouble : 0;
|
|
dir->count++;
|
|
}
|
|
|
|
cJSON_Delete(arr);
|
|
return 0;
|
|
}
|
|
|
|
/* ── NIP-44 encryption / decryption ────────────────────────────────── */
|
|
|
|
/* Encrypt a plaintext string with NIP-44 (self-to-self).
|
|
* Returns a newly allocated string (caller must free), or NULL on error. */
|
|
static char *encrypt_content(const char *plaintext) {
|
|
if (!g_have_signer || g_pubkey[0] == '\0') return NULL;
|
|
|
|
char *ciphertext = NULL;
|
|
int rc = nostr_signer_nip44_encrypt(g_signer, g_pubkey, plaintext,
|
|
&ciphertext);
|
|
if (rc != NOSTR_SUCCESS || ciphertext == NULL) {
|
|
g_printerr("[bookmarks] NIP-44 encrypt failed: %d\n", rc);
|
|
return NULL;
|
|
}
|
|
return ciphertext;
|
|
}
|
|
|
|
/* Decrypt a NIP-44 ciphertext (self-to-self).
|
|
* Returns a newly allocated string (caller must free), or NULL on error. */
|
|
static char *decrypt_content(const char *ciphertext) {
|
|
if (!g_have_signer || g_pubkey[0] == '\0') return NULL;
|
|
if (ciphertext == NULL || ciphertext[0] == '\0') return NULL;
|
|
|
|
char *plaintext = NULL;
|
|
int rc = nostr_signer_nip44_decrypt(g_signer, g_pubkey, ciphertext,
|
|
&plaintext);
|
|
if (rc != NOSTR_SUCCESS || plaintext == NULL) {
|
|
g_printerr("[bookmarks] NIP-44 decrypt failed: %d\n", rc);
|
|
return NULL;
|
|
}
|
|
return plaintext;
|
|
}
|
|
|
|
/* ── Publish ───────────────────────────────────────────────────────── */
|
|
|
|
/* Parse bootstrap relays from settings into an array.
|
|
* Returns the count. Fills urls_out (pointers into relay_buf).
|
|
* Caller must g_free(relay_buf) after use. */
|
|
static int parse_relays(char **relay_buf, const char **urls_out, int max) {
|
|
const browser_settings_t *s = settings_get();
|
|
*relay_buf = g_strdup(s->bootstrap_relays);
|
|
int count = 0;
|
|
char *saveptr = NULL;
|
|
char *line = strtok_r(*relay_buf, "\n\r", &saveptr);
|
|
while (line != NULL && count < max) {
|
|
while (*line == ' ' || *line == '\t') line++;
|
|
if (line[0] != '\0' &&
|
|
(strncmp(line, "wss://", 6) == 0 ||
|
|
strncmp(line, "ws://", 5) == 0)) {
|
|
urls_out[count++] = line;
|
|
}
|
|
line = strtok_r(NULL, "\n\r", &saveptr);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/* Publish a kind 30003 event for a directory.
|
|
* Encrypts the directory's bookmarks, signs, publishes to relays,
|
|
* and stores in SQLite. Returns 0 on success, -1 on error. */
|
|
static int publish_directory(const char *dir_name) {
|
|
if (!g_have_signer) {
|
|
g_printerr("[bookmarks] No signer, cannot publish\n");
|
|
return -1;
|
|
}
|
|
|
|
int idx = find_dir(dir_name);
|
|
if (idx < 0) {
|
|
g_printerr("[bookmarks] Directory '%s' not found\n", dir_name);
|
|
return -1;
|
|
}
|
|
|
|
/* Serialize and encrypt. */
|
|
char *json = dir_to_json(&g_dirs[idx]);
|
|
if (json == NULL) return -1;
|
|
|
|
char *ciphertext = encrypt_content(json);
|
|
g_free(json);
|
|
if (ciphertext == NULL) return -1;
|
|
|
|
/* Build tags: [["d", dir_name]] */
|
|
cJSON *tags = cJSON_CreateArray();
|
|
cJSON *d_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
|
|
cJSON_AddItemToArray(d_tag, cJSON_CreateString(dir_name));
|
|
cJSON_AddItemToArray(tags, d_tag);
|
|
|
|
/* Create and sign the event. */
|
|
cJSON *event = nostr_create_and_sign_event_with_signer(
|
|
30003, ciphertext, tags, g_signer, time(NULL));
|
|
g_free(ciphertext);
|
|
|
|
if (event == NULL) {
|
|
g_printerr("[bookmarks] Failed to create/sign event\n");
|
|
cJSON_Delete(tags);
|
|
return -1;
|
|
}
|
|
|
|
/* Store in SQLite. */
|
|
db_store_event(event);
|
|
|
|
/* Publish to bootstrap relays. */
|
|
char *relay_buf = NULL;
|
|
const char *relay_urls[32];
|
|
int relay_count = parse_relays(&relay_buf, relay_urls, 32);
|
|
|
|
if (relay_count > 0) {
|
|
int success_count = 0;
|
|
publish_result_t *results = synchronous_publish_event_with_progress(
|
|
relay_urls, relay_count, event, &success_count,
|
|
15, NULL, NULL, 0, NULL);
|
|
if (results) free(results);
|
|
g_print("[bookmarks] Published directory '%s' to %d/%d relays\n",
|
|
dir_name, success_count, relay_count);
|
|
} else {
|
|
g_print("[bookmarks] No relays configured, event stored locally only\n");
|
|
}
|
|
|
|
g_free(relay_buf);
|
|
cJSON_Delete(event);
|
|
return 0;
|
|
}
|
|
|
|
/* ── Public API ────────────────────────────────────────────────────── */
|
|
|
|
int bookmarks_init(nostr_signer_t *signer, const char *pubkey_hex) {
|
|
g_signer = signer;
|
|
g_have_signer = (signer != NULL);
|
|
if (pubkey_hex) {
|
|
snprintf(g_pubkey, sizeof(g_pubkey), "%s", pubkey_hex);
|
|
} else {
|
|
g_pubkey[0] = '\0';
|
|
}
|
|
|
|
/* Load cached kind 30003 events from SQLite. */
|
|
if (g_pubkey[0] != '\0') {
|
|
/* We need to query all kind 30003 events for this pubkey.
|
|
* db_get_events returns an array of events. */
|
|
cJSON *events = db_get_events(g_pubkey, 30003, 0);
|
|
if (events != NULL) {
|
|
int n = cJSON_GetArraySize(events);
|
|
g_print("[bookmarks] Loading %d cached directory event(s)\n", n);
|
|
cJSON *event;
|
|
cJSON_ArrayForEach(event, events) {
|
|
/* Get the d tag to find the directory name. */
|
|
const char *dir_name = "General";
|
|
cJSON *tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
|
|
if (cJSON_IsArray(tags)) {
|
|
cJSON *tag;
|
|
cJSON_ArrayForEach(tag, tags) {
|
|
if (!cJSON_IsArray(tag)) continue;
|
|
cJSON *t0 = cJSON_GetArrayItem(tag, 0);
|
|
if (cJSON_IsString(t0) &&
|
|
strcmp(t0->valuestring, "d") == 0) {
|
|
cJSON *t1 = cJSON_GetArrayItem(tag, 1);
|
|
if (cJSON_IsString(t1)) {
|
|
dir_name = t1->valuestring;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Decrypt the content. */
|
|
cJSON *content = cJSON_GetObjectItemCaseSensitive(event,
|
|
"content");
|
|
if (cJSON_IsString(content) && content->valuestring[0]) {
|
|
char *plaintext = decrypt_content(content->valuestring);
|
|
if (plaintext) {
|
|
int idx = ensure_dir(dir_name);
|
|
if (idx >= 0) {
|
|
dir_load_from_json(&g_dirs[idx], plaintext);
|
|
}
|
|
free(plaintext);
|
|
}
|
|
}
|
|
}
|
|
cJSON_Delete(events);
|
|
}
|
|
}
|
|
|
|
/* Ensure "General" always exists. */
|
|
ensure_dir("General");
|
|
|
|
g_print("[bookmarks] Initialized: %d directory/ies, signer=%s\n",
|
|
g_dir_count, g_have_signer ? "yes" : "no");
|
|
return 0;
|
|
}
|
|
|
|
void bookmarks_cleanup(void) {
|
|
if (g_dirs) {
|
|
for (int i = 0; i < g_dir_count; i++) {
|
|
free_dir_bookmarks(&g_dirs[i]);
|
|
}
|
|
g_free(g_dirs);
|
|
g_dirs = NULL;
|
|
}
|
|
g_dir_count = 0;
|
|
g_dir_cap = 0;
|
|
g_signer = NULL;
|
|
g_have_signer = 0;
|
|
g_pubkey[0] = '\0';
|
|
}
|
|
|
|
const bookmark_dir_t *bookmarks_get_dirs(int *count_out) {
|
|
if (count_out) *count_out = g_dir_count;
|
|
return g_dirs;
|
|
}
|
|
|
|
const bookmark_dir_t *bookmarks_get_dir(const char *name) {
|
|
int idx = find_dir(name);
|
|
return (idx >= 0) ? &g_dirs[idx] : NULL;
|
|
}
|
|
|
|
int bookmarks_add(const char *dir, const char *url, const char *title) {
|
|
if (!g_have_signer) {
|
|
g_printerr("[bookmarks] No signer, cannot add\n");
|
|
return -1;
|
|
}
|
|
if (dir == NULL) dir = "General";
|
|
if (url == NULL || url[0] == '\0') return -1;
|
|
|
|
int idx = ensure_dir(dir);
|
|
if (idx < 0) return -1;
|
|
|
|
/* Check if already exists. */
|
|
if (find_bookmark_in_dir(&g_dirs[idx], url) >= 0) {
|
|
g_print("[bookmarks] URL already bookmarked in '%s'\n", dir);
|
|
return 0;
|
|
}
|
|
|
|
/* Check capacity. */
|
|
if (g_dirs[idx].count >= BOOKMARKS_MAX_PER_DIR) {
|
|
g_printerr("[bookmarks] Directory '%s' is full\n", dir);
|
|
return -1;
|
|
}
|
|
|
|
/* Add to in-memory list. */
|
|
int n = g_dirs[idx].count;
|
|
g_dirs[idx].bookmarks = g_realloc(g_dirs[idx].bookmarks,
|
|
(n + 1) * sizeof(bookmark_t));
|
|
g_dirs[idx].bookmarks[n].url = g_strdup(url);
|
|
g_dirs[idx].bookmarks[n].title = g_strdup(title ? title : "");
|
|
g_dirs[idx].bookmarks[n].added = (long)time(NULL);
|
|
g_dirs[idx].count++;
|
|
|
|
g_print("[bookmarks] Added '%s' to '%s'\n", url, dir);
|
|
|
|
/* Publish. */
|
|
return publish_directory(dir);
|
|
}
|
|
|
|
int bookmarks_remove(const char *dir, const char *url) {
|
|
if (!g_have_signer) return -1;
|
|
if (dir == NULL) dir = "General";
|
|
|
|
int idx = find_dir(dir);
|
|
if (idx < 0) return -1;
|
|
|
|
int bidx = find_bookmark_in_dir(&g_dirs[idx], url);
|
|
if (bidx < 0) return -1;
|
|
|
|
/* Free the bookmark. */
|
|
g_free(g_dirs[idx].bookmarks[bidx].url);
|
|
g_free(g_dirs[idx].bookmarks[bidx].title);
|
|
|
|
/* Shift remaining down. */
|
|
for (int i = bidx; i < g_dirs[idx].count - 1; i++) {
|
|
g_dirs[idx].bookmarks[i] = g_dirs[idx].bookmarks[i + 1];
|
|
}
|
|
g_dirs[idx].count--;
|
|
|
|
g_print("[bookmarks] Removed '%s' from '%s'\n", url, dir);
|
|
return publish_directory(dir);
|
|
}
|
|
|
|
int bookmarks_move(const char *from_dir, const char *url, const char *to_dir) {
|
|
if (!g_have_signer) return -1;
|
|
if (from_dir == NULL) from_dir = "General";
|
|
if (to_dir == NULL) to_dir = "General";
|
|
|
|
int from_idx = find_dir(from_dir);
|
|
if (from_idx < 0) return -1;
|
|
|
|
int bidx = find_bookmark_in_dir(&g_dirs[from_idx], url);
|
|
if (bidx < 0) return -1;
|
|
|
|
/* Save the bookmark data. */
|
|
char *saved_url = g_strdup(g_dirs[from_idx].bookmarks[bidx].url);
|
|
char *saved_title = g_strdup(g_dirs[from_idx].bookmarks[bidx].title);
|
|
long saved_added = g_dirs[from_idx].bookmarks[bidx].added;
|
|
|
|
/* Remove from source. */
|
|
g_free(g_dirs[from_idx].bookmarks[bidx].url);
|
|
g_free(g_dirs[from_idx].bookmarks[bidx].title);
|
|
for (int i = bidx; i < g_dirs[from_idx].count - 1; i++) {
|
|
g_dirs[from_idx].bookmarks[i] = g_dirs[from_idx].bookmarks[i + 1];
|
|
}
|
|
g_dirs[from_idx].count--;
|
|
|
|
/* Add to destination. */
|
|
int to_idx = ensure_dir(to_dir);
|
|
if (to_idx < 0) {
|
|
g_free(saved_url);
|
|
g_free(saved_title);
|
|
return -1;
|
|
}
|
|
|
|
int n = g_dirs[to_idx].count;
|
|
g_dirs[to_idx].bookmarks = g_realloc(g_dirs[to_idx].bookmarks,
|
|
(n + 1) * sizeof(bookmark_t));
|
|
g_dirs[to_idx].bookmarks[n].url = saved_url;
|
|
g_dirs[to_idx].bookmarks[n].title = saved_title;
|
|
g_dirs[to_idx].bookmarks[n].added = saved_added;
|
|
g_dirs[to_idx].count++;
|
|
|
|
g_print("[bookmarks] Moved '%s' from '%s' to '%s'\n",
|
|
url, from_dir, to_dir);
|
|
|
|
/* Publish both directories. */
|
|
publish_directory(from_dir);
|
|
return publish_directory(to_dir);
|
|
}
|
|
|
|
int bookmarks_create_dir(const char *name) {
|
|
if (!g_have_signer) return -1;
|
|
if (name == NULL || name[0] == '\0') return -1;
|
|
if (find_dir(name) >= 0) {
|
|
g_printerr("[bookmarks] Directory '%s' already exists\n", name);
|
|
return -1;
|
|
}
|
|
|
|
int idx = ensure_dir(name);
|
|
if (idx < 0) return -1;
|
|
|
|
g_print("[bookmarks] Created directory '%s'\n", name);
|
|
return publish_directory(name);
|
|
}
|
|
|
|
int bookmarks_delete_dir(const char *name, int move_to_general) {
|
|
if (!g_have_signer) return -1;
|
|
if (name == NULL) return -1;
|
|
if (strcmp(name, "General") == 0) {
|
|
g_printerr("[bookmarks] Cannot delete 'General'\n");
|
|
return -1;
|
|
}
|
|
|
|
int idx = find_dir(name);
|
|
if (idx < 0) return -1;
|
|
|
|
/* Optionally move bookmarks to General. */
|
|
if (move_to_general && g_dirs[idx].count > 0) {
|
|
int gen_idx = ensure_dir("General");
|
|
if (gen_idx >= 0) {
|
|
for (int i = 0; i < g_dirs[idx].count; i++) {
|
|
int n = g_dirs[gen_idx].count;
|
|
g_dirs[gen_idx].bookmarks = g_realloc(
|
|
g_dirs[gen_idx].bookmarks,
|
|
(n + 1) * sizeof(bookmark_t));
|
|
g_dirs[gen_idx].bookmarks[n] = g_dirs[idx].bookmarks[i];
|
|
g_dirs[gen_idx].count++;
|
|
}
|
|
g_dirs[idx].count = 0; /* Don't free — ownership moved */
|
|
g_dirs[idx].bookmarks = NULL;
|
|
publish_directory("General");
|
|
}
|
|
}
|
|
|
|
/* Free the directory's data. */
|
|
free_dir_bookmarks(&g_dirs[idx]);
|
|
|
|
/* Remove from the array. */
|
|
for (int i = idx; i < g_dir_count - 1; i++) {
|
|
g_dirs[i] = g_dirs[i + 1];
|
|
}
|
|
g_dir_count--;
|
|
|
|
g_print("[bookmarks] Deleted directory '%s'\n", name);
|
|
|
|
/* TODO: publish a kind 5 deletion event for the old d-tag.
|
|
* For now, the directory event will remain on relays but won't
|
|
* appear in the UI. A future fetch will re-add it — we need the
|
|
* deletion event to prevent that. */
|
|
return 0;
|
|
}
|
|
|
|
int bookmarks_load_dir_from_json(const char *dir_name, const char *json) {
|
|
if (dir_name == NULL || json == NULL) return -1;
|
|
int idx = ensure_dir(dir_name);
|
|
if (idx < 0) return -1;
|
|
return dir_load_from_json(&g_dirs[idx], json);
|
|
}
|
|
|
|
int bookmarks_store_and_load_event(const void *event_cjson) {
|
|
const cJSON *event = (const cJSON *)event_cjson;
|
|
if (event == NULL) return -1;
|
|
|
|
/* Store in SQLite. */
|
|
db_store_event(event);
|
|
|
|
/* If we have a signer, decrypt and load into memory. */
|
|
if (!g_have_signer) return 0;
|
|
|
|
/* Get the d tag. */
|
|
const char *dir_name = "General";
|
|
cJSON *tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
|
|
if (cJSON_IsArray(tags)) {
|
|
cJSON *tag;
|
|
cJSON_ArrayForEach(tag, tags) {
|
|
if (!cJSON_IsArray(tag)) continue;
|
|
cJSON *t0 = cJSON_GetArrayItem(tag, 0);
|
|
if (cJSON_IsString(t0) && strcmp(t0->valuestring, "d") == 0) {
|
|
cJSON *t1 = cJSON_GetArrayItem(tag, 1);
|
|
if (cJSON_IsString(t1)) dir_name = t1->valuestring;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Decrypt content. */
|
|
cJSON *content = cJSON_GetObjectItemCaseSensitive(event, "content");
|
|
if (cJSON_IsString(content) && content->valuestring[0]) {
|
|
char *plaintext = decrypt_content(content->valuestring);
|
|
if (plaintext) {
|
|
int idx = ensure_dir(dir_name);
|
|
if (idx >= 0) {
|
|
dir_load_from_json(&g_dirs[idx], plaintext);
|
|
}
|
|
free(plaintext);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|