356 lines
11 KiB
C
356 lines
11 KiB
C
#include "tui.h"
|
|
#include "state.h"
|
|
#include "net.h"
|
|
|
|
#include "publish.h"
|
|
|
|
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
|
#include "../resources/nostr_core_lib/nostr_core/nip019.h"
|
|
#include "../resources/nostr_core_lib/nostr_core/utils.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
typedef struct {
|
|
cJSON *tags;
|
|
int *tag_indices;
|
|
int count;
|
|
} follows_table_ctx_t;
|
|
|
|
static int follows_publish_kind3(cJSON *event_obj) {
|
|
cJSON *tags;
|
|
cJSON *content;
|
|
|
|
if (!event_obj) {
|
|
return -1;
|
|
}
|
|
|
|
tags = cJSON_GetObjectItemCaseSensitive(event_obj, "tags");
|
|
content = cJSON_GetObjectItemCaseSensitive(event_obj, "content");
|
|
|
|
return publish_signed_event_with_report("Follows",
|
|
3,
|
|
(cJSON_IsString(content) && content->valuestring) ? content->valuestring : "",
|
|
tags,
|
|
8000);
|
|
}
|
|
|
|
static int follows_collect_rows(cJSON *tags, int **out_indices) {
|
|
int n;
|
|
int i;
|
|
int count = 0;
|
|
int *indices;
|
|
|
|
if (!out_indices || !cJSON_IsArray(tags)) {
|
|
return 0;
|
|
}
|
|
|
|
*out_indices = NULL;
|
|
n = cJSON_GetArraySize(tags);
|
|
if (n <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
indices = (int *)malloc((size_t)n * sizeof(int));
|
|
if (!indices) {
|
|
return 0;
|
|
}
|
|
|
|
for (i = 0; i < n; i++) {
|
|
cJSON *tag = cJSON_GetArrayItem(tags, i);
|
|
cJSON *t0 = cJSON_GetArrayItem(tag, 0);
|
|
if (!cJSON_IsArray(tag) || !cJSON_IsString(t0) || strcmp(t0->valuestring, "p") != 0) {
|
|
continue;
|
|
}
|
|
indices[count++] = i;
|
|
}
|
|
|
|
*out_indices = indices;
|
|
return count;
|
|
}
|
|
|
|
static void follows_table_get_cell(int row, int col, char *out, size_t out_size, void *user_data) {
|
|
const follows_table_ctx_t *ctx = (const follows_table_ctx_t *)user_data;
|
|
cJSON *tag;
|
|
cJSON *t1;
|
|
cJSON *t3;
|
|
|
|
if (!out || out_size == 0U) {
|
|
return;
|
|
}
|
|
out[0] = '\0';
|
|
|
|
if (!ctx || row < 0 || row >= ctx->count) {
|
|
return;
|
|
}
|
|
|
|
tag = cJSON_GetArrayItem(ctx->tags, ctx->tag_indices[row]);
|
|
t1 = cJSON_GetArrayItem(tag, 1);
|
|
t3 = cJSON_GetArrayItem(tag, 3);
|
|
|
|
if (col == 0) {
|
|
snprintf(out, out_size, "%d", row + 1);
|
|
} else if (col == 1) {
|
|
char npub[128] = {0};
|
|
if (cJSON_IsString(t1) && t1->valuestring) {
|
|
unsigned char pub[32];
|
|
if (strlen(t1->valuestring) == 64 && nostr_hex_to_bytes(t1->valuestring, pub, 32) == 0) {
|
|
if (nostr_key_to_bech32(pub, "npub", npub) != 0) {
|
|
snprintf(npub, sizeof(npub), "%s", t1->valuestring);
|
|
}
|
|
} else {
|
|
snprintf(npub, sizeof(npub), "%s", t1->valuestring);
|
|
}
|
|
}
|
|
snprintf(out, out_size, "%s", npub);
|
|
} else if (col == 2) {
|
|
snprintf(out, out_size, "%s", (cJSON_IsString(t3) && t3->valuestring) ? t3->valuestring : "");
|
|
}
|
|
}
|
|
|
|
static void follows_show_profile(cJSON *meta) {
|
|
char *meta_json;
|
|
size_t total;
|
|
char *view_text;
|
|
|
|
if (!meta) {
|
|
nt_pager_show_text("> Main Menu > Follows > Profile", "No metadata content.");
|
|
return;
|
|
}
|
|
|
|
meta_json = cJSON_Print(meta);
|
|
if (!meta_json) {
|
|
tuin_notice("Failed to render profile metadata.");
|
|
return;
|
|
}
|
|
|
|
total = strlen("Profile\n\n") + strlen(meta_json) + 1U;
|
|
view_text = (char *)malloc(total);
|
|
if (!view_text) {
|
|
free(meta_json);
|
|
tuin_notice("Out of memory while preparing profile view.");
|
|
return;
|
|
}
|
|
|
|
snprintf(view_text, total, "Profile\n\n%s", meta_json);
|
|
nt_pager_show_text("> Main Menu > Follows > Profile", view_text);
|
|
|
|
free(view_text);
|
|
free(meta_json);
|
|
}
|
|
|
|
void menu_follows(void) {
|
|
static const TuiMenuItem ACTION_ITEMS[] = {
|
|
{NT_HK("R", "efresh selected profile"), 'r'},
|
|
{NT_HK("A", "dd follow"), 'a'},
|
|
{NT_HK("D", "elete selected follow"), 'd'},
|
|
{NT_HK("P", "ost changes and exit"), 'p'},
|
|
{"E" NT_HK("x", "it without saving"), 'x'},
|
|
};
|
|
|
|
cJSON *working = NULL;
|
|
cJSON *tags = NULL;
|
|
TuiMenuState action_state = {0};
|
|
int selected_tag_index = -1;
|
|
|
|
working = g_state.kind3_json ? cJSON_Parse(g_state.kind3_json) : NULL;
|
|
if (!working) {
|
|
working = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(working, "kind", 3);
|
|
cJSON_AddStringToObject(working, "content", "");
|
|
cJSON_AddItemToObject(working, "tags", cJSON_CreateArray());
|
|
}
|
|
|
|
tags = cJSON_GetObjectItemCaseSensitive(working, "tags");
|
|
if (!cJSON_IsArray(tags)) {
|
|
cJSON_DeleteItemFromObjectCaseSensitive(working, "tags");
|
|
cJSON_AddItemToObject(working, "tags", cJSON_CreateArray());
|
|
tags = cJSON_GetObjectItemCaseSensitive(working, "tags");
|
|
}
|
|
|
|
while (1) {
|
|
int *row_indices = NULL;
|
|
int row_count = follows_collect_rows(tags, &row_indices);
|
|
follows_table_ctx_t ctx;
|
|
TuiColumn cols[] = {
|
|
{"#", 4, 1},
|
|
{"npub", 0, 0},
|
|
{"name", 24, 0},
|
|
};
|
|
TuiTable table;
|
|
TuiFrame frame = nt_frame("> Main Menu > Follows");
|
|
TuiStatus status = nt_status();
|
|
TuiMenu menu = {ACTION_ITEMS, (int)(sizeof(ACTION_ITEMS) / sizeof(ACTION_ITEMS[0]))};
|
|
int selected_row;
|
|
int action;
|
|
|
|
ctx.tags = tags;
|
|
ctx.tag_indices = row_indices;
|
|
ctx.count = row_count;
|
|
|
|
table.columns = cols;
|
|
table.column_count = (int)(sizeof(cols) / sizeof(cols[0]));
|
|
table.row_count = row_count;
|
|
table.user_data = &ctx;
|
|
table.get_cell = follows_table_get_cell;
|
|
table.is_default = NULL;
|
|
table.prefix_len = NULL;
|
|
|
|
selected_row = tuin_table_run(&frame, &table, &status);
|
|
if (selected_row >= 0 && selected_row < row_count) {
|
|
selected_tag_index = row_indices[selected_row];
|
|
}
|
|
|
|
free(row_indices);
|
|
|
|
action = tuin_menu_run(&frame, &menu, &status, &action_state);
|
|
if (action < 0 || action == 4) {
|
|
break;
|
|
}
|
|
|
|
if (action == 1) {
|
|
char who[256];
|
|
char follow_hex[65] = {0};
|
|
char filter[256];
|
|
char *follow_kind0 = NULL;
|
|
cJSON *ev;
|
|
cJSON *content;
|
|
cJSON *meta;
|
|
cJSON *name;
|
|
|
|
who[0] = '\0';
|
|
if (tuin_prompt("npub to add >", "", who, sizeof(who)) != 0 || who[0] == '\0') {
|
|
continue;
|
|
}
|
|
|
|
if (strncmp(who, "npub", 4) == 0) {
|
|
unsigned char pub[32];
|
|
if (nostr_decode_npub(who, pub) != 0) {
|
|
tuin_notice("Invalid npub.");
|
|
continue;
|
|
}
|
|
nostr_bytes_to_hex(pub, 32, follow_hex);
|
|
} else if (strlen(who) == 64) {
|
|
unsigned char pub[32];
|
|
if (nostr_hex_to_bytes(who, pub, 32) != 0) {
|
|
tuin_notice("Invalid hex pubkey.");
|
|
continue;
|
|
}
|
|
strncpy(follow_hex, who, sizeof(follow_hex) - 1U);
|
|
follow_hex[sizeof(follow_hex) - 1U] = '\0';
|
|
} else {
|
|
tuin_notice("Enter npub or 64-char hex pubkey.");
|
|
continue;
|
|
}
|
|
|
|
snprintf(filter, sizeof(filter), "{\"authors\":[\"%s\"],\"kinds\":[0],\"limit\":1}", follow_hex);
|
|
if (nt_get_first(filter,
|
|
state_get_read_relays(NULL),
|
|
g_state.read_relay_count > 0 ? g_state.read_relay_count : g_state.bootstrap_relay_count,
|
|
7000,
|
|
&follow_kind0) != 0 || !follow_kind0) {
|
|
tuin_notice("Can't find user. Try adding more relays.");
|
|
continue;
|
|
}
|
|
|
|
ev = cJSON_Parse(follow_kind0);
|
|
meta = NULL;
|
|
name = NULL;
|
|
if (ev) {
|
|
content = cJSON_GetObjectItemCaseSensitive(ev, "content");
|
|
if (cJSON_IsString(content) && content->valuestring) {
|
|
meta = cJSON_Parse(content->valuestring);
|
|
if (meta) {
|
|
name = cJSON_GetObjectItemCaseSensitive(meta, "name");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tuin_confirm((cJSON_IsString(name) && name->valuestring)
|
|
? "Add follow?"
|
|
: "Add this follow?") == 1) {
|
|
cJSON *tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString("p"));
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString(follow_hex));
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString(""));
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString((cJSON_IsString(name) && name->valuestring) ? name->valuestring : ""));
|
|
cJSON_AddItemToArray(tags, tag);
|
|
}
|
|
|
|
cJSON_Delete(meta);
|
|
cJSON_Delete(ev);
|
|
free(follow_kind0);
|
|
} else if (action == 2) {
|
|
if (selected_tag_index < 0 || selected_tag_index >= cJSON_GetArraySize(tags)) {
|
|
tuin_notice("Select a follow row first.");
|
|
continue;
|
|
}
|
|
|
|
if (tuin_confirm("Delete follow?") == 1) {
|
|
cJSON_DeleteItemFromArray(tags, selected_tag_index);
|
|
selected_tag_index = -1;
|
|
}
|
|
} else if (action == 0) {
|
|
cJSON *tag;
|
|
cJSON *pub;
|
|
char filter[256];
|
|
char *follow_kind0 = NULL;
|
|
cJSON *ev;
|
|
cJSON *content;
|
|
cJSON *meta;
|
|
|
|
if (selected_tag_index < 0 || selected_tag_index >= cJSON_GetArraySize(tags)) {
|
|
tuin_notice("Select a follow row first.");
|
|
continue;
|
|
}
|
|
|
|
tag = cJSON_GetArrayItem(tags, selected_tag_index);
|
|
pub = cJSON_GetArrayItem(tag, 1);
|
|
if (!cJSON_IsString(pub) || !pub->valuestring) {
|
|
continue;
|
|
}
|
|
|
|
snprintf(filter, sizeof(filter), "{\"authors\":[\"%s\"],\"kinds\":[0],\"limit\":1}", pub->valuestring);
|
|
if (nt_get_first(filter,
|
|
state_get_read_relays(NULL),
|
|
g_state.read_relay_count > 0 ? g_state.read_relay_count : g_state.bootstrap_relay_count,
|
|
7000,
|
|
&follow_kind0) != 0 || !follow_kind0) {
|
|
tuin_notice("Unable to fetch profile.");
|
|
continue;
|
|
}
|
|
|
|
ev = cJSON_Parse(follow_kind0);
|
|
meta = NULL;
|
|
if (ev) {
|
|
content = cJSON_GetObjectItemCaseSensitive(ev, "content");
|
|
if (cJSON_IsString(content) && content->valuestring) {
|
|
meta = cJSON_Parse(content->valuestring);
|
|
}
|
|
}
|
|
|
|
follows_show_profile(meta);
|
|
cJSON_Delete(meta);
|
|
cJSON_Delete(ev);
|
|
free(follow_kind0);
|
|
} else if (action == 3) {
|
|
int posted = follows_publish_kind3(working);
|
|
char *new_json = cJSON_PrintUnformatted(working);
|
|
if (new_json) {
|
|
free(g_state.kind3_json);
|
|
g_state.kind3_json = new_json;
|
|
}
|
|
|
|
if (posted < 0) {
|
|
tuin_notice("Follows publish failed.");
|
|
} else {
|
|
nt_log("Follows published.");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
cJSON_Delete(working);
|
|
}
|