Files
nostr_terminal/src/menu_follows.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);
}