From f28e409ea56058f6fea7171202bd84b51b501bc7 Mon Sep 17 00:00:00 2001 From: Cory McWilliams Date: Sat, 11 Jan 2025 15:49:49 -0500 Subject: [PATCH] ssb: Add a get_contacts command to enumerate follows, blocks, and friends. --- src/main.c | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/ssb.db.c | 58 ++++++++++++++++++++++-------- src/ssb.db.h | 13 +++++-- src/ssb.js.c | 2 +- 4 files changed, 153 insertions(+), 19 deletions(-) diff --git a/src/main.c b/src/main.c index 0a358628..04101004 100644 --- a/src/main.c +++ b/src/main.c @@ -152,6 +152,7 @@ static int _tf_command_store_blob(const char* file, int argc, char* argv[]); static int _tf_command_get_sequence(const char* file, int argc, char* argv[]); static int _tf_command_get_identity(const char* file, int argc, char* argv[]); static int _tf_command_get_profile(const char* file, int argc, char* argv[]); +static int _tf_command_get_contacts(const char* file, int argc, char* argv[]); static int _tf_command_test(const char* file, int argc, char* argv[]); static int _tf_command_verify(const char* file, int argc, char* argv[]); static int _tf_command_usage(const char* file); @@ -173,6 +174,7 @@ const command_t k_commands[] = { { "get_sequence", _tf_command_get_sequence, "Get the last sequence number for a feed." }, { "get_identity", _tf_command_get_identity, "Get the server account identity." }, { "get_profile", _tf_command_get_profile, "Get profile information for the given identity." }, + { "get_contacts", _tf_command_get_contacts, "Get information about followed, blocked, and friend identities." }, { "has_blob", _tf_command_has_blob, "Check whether a blob is in the blob store." }, { "store_blob", _tf_command_store_blob, "Write a file to the blob store." }, { "verify", _tf_command_verify, "Verify a feed." }, @@ -944,7 +946,7 @@ static int _tf_command_get_profile(const char* file, int argc, char* argv[]) tf_printf("\n%s get_profile [options]\n\n", file); tf_printf("options:\n"); tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path); - tf_printf(" -i, --identity identity Account from which to get latest sequence number.\n"); + tf_printf(" -i, --identity identity Account for which to get profile information.\n"); tf_printf(" -h, --help Show this usage information.\n"); tf_free((void*)default_db_path); return EXIT_FAILURE; @@ -961,6 +963,101 @@ static int _tf_command_get_profile(const char* file, int argc, char* argv[]) return profile != NULL; } +static int _tf_command_get_contacts(const char* file, int argc, char* argv[]) +{ + const char* default_db_path = _get_db_path(); + const char* db_path = default_db_path; + const char* identity = NULL; + bool show_usage = false; + + while (!show_usage) + { + static const struct option k_options[] = { + { "db-path", required_argument, NULL, 'd' }, + { "id", required_argument, NULL, 'i' }, + { "help", no_argument, NULL, 'h' }, + { 0 }, + }; + int c = getopt_long(argc, argv, "d:i:h", k_options, NULL); + if (c == -1) + { + break; + } + + switch (c) + { + case '?': + case 'h': + default: + show_usage = true; + break; + case 'd': + db_path = optarg; + break; + case 'i': + identity = optarg; + break; + } + } + + if (show_usage || !identity) + { + tf_printf("\n%s get_contacts [options]\n\n", file); + tf_printf("options:\n"); + tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path); + tf_printf(" -i, --identity identity Account from which to get contact information.\n"); + tf_printf(" -h, --help Show this usage information.\n"); + tf_free((void*)default_db_path); + return EXIT_FAILURE; + } + + tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL); + JSContext* context = tf_ssb_get_context(ssb); + JSValue contacts = JS_NewObject(context); + JSValue follows = JS_NewObject(context); + JSValue blocks = JS_NewObject(context); + JSValue friends = JS_NewObject(context); + tf_ssb_following_t* following = tf_ssb_db_following_deep(ssb, &identity, 1, 1, true); + tf_ssb_following_t* following2 = tf_ssb_db_following_deep(ssb, &identity, 1, 2, false); + sqlite3* db = tf_ssb_acquire_db_reader(ssb); + for (int i = 0; *following[i].id; i++) + { + if (following[i].followed_by_count) + { + const char* name = tf_ssb_db_get_profile_name(db, following[i].id); + JS_SetPropertyStr(context, follows, following[i].id, name ? JS_NewString(context, name) : JS_NULL); + tf_free((void*)name); + } + if (following[i].blocked_by_count) + { + const char* name = tf_ssb_db_get_profile_name(db, following[i].id); + JS_SetPropertyStr(context, blocks, following[i].id, name ? JS_NewString(context, name) : JS_NULL); + tf_free((void*)name); + } + } + for (int i = 0; *following2[i].id; i++) + { + const char* name = tf_ssb_db_get_profile_name(db, following2[i].id); + JS_SetPropertyStr(context, friends, following2[i].id, name ? JS_NewString(context, name) : JS_NULL); + tf_free((void*)name); + } + tf_ssb_release_db_reader(ssb, db); + JS_SetPropertyStr(context, contacts, "follows", follows); + JS_SetPropertyStr(context, contacts, "blocks", blocks); + JS_SetPropertyStr(context, contacts, "friends", friends); + tf_free(following2); + tf_free(following); + JSValue json = JS_JSONStringify(context, contacts, JS_NULL, JS_NewInt32(context, 2)); + const char* json_str = JS_ToCString(context, json); + tf_printf("%s\n", json_str); + JS_FreeCString(context, json_str); + JS_FreeValue(context, json); + JS_FreeValue(context, contacts); + tf_ssb_destroy(ssb); + tf_free((void*)default_db_path); + return EXIT_SUCCESS; +} + static int _tf_command_verify(const char* file, int argc, char* argv[]) { const char* identity = NULL; diff --git a/src/ssb.db.c b/src/ssb.db.c index 70d5a894..cd421b9b 100644 --- a/src/ssb.db.c +++ b/src/ssb.db.c @@ -1304,9 +1304,9 @@ static bool _is_blocked_by_active_blocks(const char* id, const block_node_t* blo return false; } -static following_t* _make_following_node(const char* id, following_t*** following, int* following_count, block_node_t* blocks) +static following_t* _make_following_node(const char* id, following_t*** following, int* following_count, block_node_t* blocks, bool include_blocks) { - if (_is_blocked_by_active_blocks(id, blocks)) + if (!include_blocks && _is_blocked_by_active_blocks(id, blocks)) { return NULL; } @@ -1333,7 +1333,7 @@ static following_t* _make_following_node(const char* id, following_t*** followin return entry; } -static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, following_t*** following, int* following_count, block_node_t* active_blocks) +static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, following_t*** following, int* following_count, block_node_t* active_blocks, bool include_blocks) { sqlite3* db = tf_ssb_acquire_db_reader(ssb); sqlite3_stmt* statement = NULL; @@ -1352,7 +1352,7 @@ static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, foll if (sqlite3_column_type(statement, 1) != SQLITE_NULL) { bool is_following = sqlite3_column_int(statement, 1) != 0; - following_t* next = _make_following_node(contact, following, following_count, active_blocks); + following_t* next = _make_following_node(contact, following, following_count, active_blocks, include_blocks); if (next) { if (is_following) @@ -1374,7 +1374,7 @@ static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, foll if (sqlite3_column_type(statement, 2) != SQLITE_NULL) { bool is_blocking = sqlite3_column_int(statement, 2) != 0; - following_t* next = _make_following_node(contact, following, following_count, active_blocks); + following_t* next = _make_following_node(contact, following, following_count, active_blocks, include_blocks); if (next) { if (is_blocking) @@ -1400,13 +1400,14 @@ static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, foll tf_ssb_release_db_reader(ssb, db); } -static void _get_following(tf_ssb_t* ssb, following_t* entry, following_t*** following, int* following_count, int depth, int max_depth, block_node_t* active_blocks) +static void _get_following( + tf_ssb_t* ssb, following_t* entry, following_t*** following, int* following_count, int depth, int max_depth, block_node_t* active_blocks, bool include_blocks) { entry->depth = tf_min(depth, entry->depth); if (depth < max_depth && !entry->populated && !_is_blocked_by_active_blocks(entry->id, active_blocks)) { entry->populated = true; - _populate_follows_and_blocks(ssb, entry, following, following_count, active_blocks); + _populate_follows_and_blocks(ssb, entry, following, following_count, active_blocks, include_blocks); if (depth < max_depth) { @@ -1415,28 +1416,28 @@ static void _get_following(tf_ssb_t* ssb, following_t* entry, following_t*** fol { if (!_has_following_entry(entry->following[i]->id, entry->blocking, entry->blocking_count)) { - _get_following(ssb, entry->following[i], following, following_count, depth + 1, max_depth, &blocks); + _get_following(ssb, entry->following[i], following, following_count, depth + 1, max_depth, &blocks, include_blocks); } } } } } -tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, int count, int depth) +tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, int count, int depth, bool include_blocks) { following_t** following = NULL; int following_count = 0; for (int i = 0; i < count; i++) { - following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL); - _get_following(ssb, entry, &following, &following_count, 0, depth, NULL); + following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL, include_blocks); + _get_following(ssb, entry, &following, &following_count, 0, depth, NULL, include_blocks); entry->ref_count++; } int actual_following_count = 0; for (int i = 0; i < following_count; i++) { - if (following[i]->ref_count > 0) + if (following[i]->ref_count > 0 || include_blocks) { actual_following_count++; } @@ -1448,7 +1449,7 @@ tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, in int write_index = 0; for (int i = 0; i < following_count; i++) { - if (following[i]->ref_count > 0) + if (following[i]->ref_count > 0 || include_blocks) { snprintf(result[write_index].id, sizeof(result[write_index].id), "%s", following[i]->id); result[write_index].following_count = following[i]->following_count; @@ -1477,8 +1478,8 @@ const char** tf_ssb_db_following_deep_ids(tf_ssb_t* ssb, const char** ids, int c int following_count = 0; for (int i = 0; i < count; i++) { - following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL); - _get_following(ssb, entry, &following, &following_count, 0, depth, NULL); + following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL, false); + _get_following(ssb, entry, &following, &following_count, 0, depth, NULL, false); entry->ref_count++; } @@ -2109,3 +2110,30 @@ const char* tf_ssb_db_get_profile(sqlite3* db, const char* id) } return result; } + +const char* tf_ssb_db_get_profile_name(sqlite3* db, const char* id) +{ + const char* result = NULL; + sqlite3_stmt* statement; + if (sqlite3_prepare(db, + "SELECT name FROM (SELECT messages.author, RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank, " + "messages.content ->> 'name' AS name FROM messages WHERE messages.author = ? " + "AND json_extract(messages.content, '$.type') = 'about' AND content ->> 'about' = messages.author AND name IS NOT NULL) " + "WHERE author_rank = 1", + -1, &statement, NULL) == SQLITE_OK) + { + if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK) + { + if (sqlite3_step(statement) == SQLITE_ROW) + { + result = tf_strdup((const char*)sqlite3_column_text(statement, 0)); + } + } + sqlite3_finalize(statement); + } + else + { + tf_printf("prepare failed: %s\n", sqlite3_errmsg(db)); + } + return result; +} diff --git a/src/ssb.db.h b/src/ssb.db.h index 737af03a..36daf4ab 100644 --- a/src/ssb.db.h +++ b/src/ssb.db.h @@ -308,9 +308,10 @@ const char** tf_ssb_db_following_deep_ids(tf_ssb_t* ssb, const char** ids, int c ** @param ids An array of identities. ** @param count The number of identities. ** @param depth The following depth to use (prefer 2). -** @return An array of information about visible accounts. Fere with tf_free(). +** @param include_blocks Whether to include blocked identities in results. +** @return An array of information about visible accounts. Free with tf_free(). */ -tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, int count, int depth); +tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, int count, int depth, bool include_blocks); /** ** Get all visible identities from all local accounts. @@ -493,6 +494,14 @@ bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* ou */ const char* tf_ssb_db_get_profile(sqlite3* db, const char* id); +/** +** Get the latest profile name for the given identity. +** @param db The database. +** @param id The identity. +** @return The name. Free with tf_free(). +*/ +const char* tf_ssb_db_get_profile_name(sqlite3* db, const char* id); + /** ** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use. ** @param user_data User data registered with the authorizer. diff --git a/src/ssb.js.c b/src/ssb.js.c index f158769f..0fe2b743 100644 --- a/src/ssb.js.c +++ b/src/ssb.js.c @@ -2170,7 +2170,7 @@ typedef struct _following_t static void _tf_ssb_following_work(tf_ssb_t* ssb, void* user_data) { following_t* following = user_data; - following->out_following = tf_ssb_db_following_deep(ssb, following->ids, following->ids_count, following->depth); + following->out_following = tf_ssb_db_following_deep(ssb, following->ids, following->ids_count, following->depth, false); } static void _tf_ssb_following_after_work(tf_ssb_t* ssb, int status, void* user_data)