From 6db1a097aa5debc9251d11046c95e506111afb71 Mon Sep 17 00:00:00 2001 From: Cory McWilliams Date: Fri, 20 Oct 2023 14:37:24 +0000 Subject: [PATCH] Add a button in the profile editor to ask the server to follow you. I'm hoping this helps replicating accounts that are otherwise difficult to discover. git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4558 ed5197a5-7fde-0310-b194-c3ffbd925b24 --- apps/ssb.json | 3 +- apps/ssb/app.js | 6 ++++ apps/ssb/tf-profile.js | 41 ++++++++++++++++++++++ core/core.js | 9 ++++- src/ssb.db.c | 77 +++++++++++++++++++++++++++++++++++------- src/ssb.js.c | 67 ++++++++++++++++++++++++++++++++++++ 6 files changed, 188 insertions(+), 15 deletions(-) diff --git a/apps/ssb.json b/apps/ssb.json index 1826e682..1b566865 100644 --- a/apps/ssb.json +++ b/apps/ssb.json @@ -1,4 +1,5 @@ { "type": "tildefriends-app", - "emoji": "🐌" + "emoji": "🐌", + "previous": "&Vm+p2yc+lC+wd71VVNRmEGpaePRsnwyknk+qI9hphjQ=.sha256" } \ No newline at end of file diff --git a/apps/ssb/app.js b/apps/ssb/app.js index 922b554b..8799d8fb 100644 --- a/apps/ssb/app.js +++ b/apps/ssb/app.js @@ -18,6 +18,12 @@ tfrpc.register(async function databaseSet(key, value) { tfrpc.register(async function createIdentity() { return ssb.createIdentity(); }); +tfrpc.register(async function getServerIdentity() { + return ssb.getServerIdentity(); +}); +tfrpc.register(async function setServerFollowingMe(id, following) { + return ssb.setServerFollowingMe(id, following); +}); tfrpc.register(async function getIdentities() { return ssb.getIdentities(); }); diff --git a/apps/ssb/tf-profile.js b/apps/ssb/tf-profile.js index 7d18a5d9..ea39fae9 100644 --- a/apps/ssb/tf-profile.js +++ b/apps/ssb/tf-profile.js @@ -11,6 +11,7 @@ class TfProfileElement extends LitElement { id: {type: String}, users: {type: Object}, size: {type: Number}, + server_follows_me: {type: Boolean}, }; } @@ -24,6 +25,23 @@ class TfProfileElement extends LitElement { this.id = null; this.users = {}; this.size = 0; + this.server_follows_me = undefined; + } + + async initial_load() { + this.server_follows_me = undefined; + let server_id = await tfrpc.rpc.getServerIdentity(); + let followed = await tfrpc.rpc.query(` + SELECT json_extract(content, '$.following') AS following FROM messages + WHERE author = ? AND + json_extract(content, '$.type') = 'contact' AND + json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1 + `, [server_id, this.whoami]); + let is_followed = false; + for (let row of followed) { + is_followed = row.following != 0; + } + this.server_follows_me = is_followed; } modify(change) { @@ -103,7 +121,23 @@ class TfProfileElement extends LitElement { input.click(); } + async server_follow_me(follow) { + try { + await tfrpc.rpc.setServerFollowingMe(this.whoami, follow); + } catch (e) { + console.log(e); + } + try { + await this.initial_load(); + } catch (e) { + console.log(e); + } + } + render() { + if (this.id == this.whoami && this.editing && this.server_follows_me === undefined) { + this.initial_load(); + } let self = this; let profile = this.users[this.id] || {}; tfrpc.rpc.query( @@ -116,9 +150,16 @@ class TfProfileElement extends LitElement { let block; if (this.id === this.whoami) { if (this.editing) { + let server_follow; + if (this.server_follows_me === true) { + server_follow = html` this.server_follow_me(false)}>`; + } else if (this.server_follows_me === false) { + server_follow = html` this.server_follow_me(true)}>`; + } edit = html` + ${server_follow} `; } else { edit = html``; diff --git a/core/core.js b/core/core.js index 2c4e5e02..e5fa906b 100644 --- a/core/core.js +++ b/core/core.js @@ -421,6 +421,13 @@ async function getProcessBlob(blobId, key, options) { return ssb.privateMessageDecrypt(process.credentials.session.name, id, message); } }; + imports.ssb.setServerFollowingMe = function(id, following) { + if (process.credentials && + process.credentials.session && + process.credentials.session.name) { + return ssb.setServerFollowingMe(process.credentials.session.name, id, following); + } + }; imports.fetch = function(url, options) { return http.fetch(url, options, gGlobalSettings.fetch_hosts); } @@ -683,7 +690,7 @@ async function blobHandler(request, response, blobId, uri) { if (!uri) { response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + blobId + '/', "Content-Length": "0"}); - response.end(data); + response.end(); return; } diff --git a/src/ssb.db.c b/src/ssb.db.c index b6e14972..2b992875 100644 --- a/src/ssb.db.c +++ b/src/ssb.db.c @@ -1258,7 +1258,10 @@ void tf_ssb_db_identity_visit_all(tf_ssb_t* ssb, void (*callback)(const char* id bool tf_ssb_db_identity_get_private_key(tf_ssb_t* ssb, const char* user, const char* public_key, uint8_t* out_private_key, size_t private_key_size) { bool success = false; - memset(out_private_key, 0, crypto_sign_SECRETKEYBYTES); + if (out_private_key) + { + memset(out_private_key, 0, private_key_size); + } sqlite3* db = tf_ssb_acquire_db_reader(ssb); sqlite3_stmt* statement = NULL; if (sqlite3_prepare(db, "SELECT private_key FROM identities WHERE user = ? AND public_key = ?", -1, &statement, NULL) == SQLITE_OK) @@ -1269,8 +1272,15 @@ bool tf_ssb_db_identity_get_private_key(tf_ssb_t* ssb, const char* user, const c if (sqlite3_step(statement) == SQLITE_ROW) { const char* key = (const char*)sqlite3_column_text(statement, 0); - int r = tf_base64_decode(key, sqlite3_column_bytes(statement, 0) - strlen(".ed25519"), out_private_key, private_key_size); - success = r > 0; + if (out_private_key && private_key_size) + { + int r = tf_base64_decode(key, sqlite3_column_bytes(statement, 0) - strlen(".ed25519"), out_private_key, private_key_size); + success = r > 0; + } + else + { + success = true; + } } } sqlite3_finalize(statement); @@ -1289,6 +1299,7 @@ typedef struct _following_t int following_count; int blocking_count; int depth; + int ref_count; } following_t; static int _following_compare(const void* a, const void* b) @@ -1301,7 +1312,7 @@ static int _following_compare(const void* a, const void* b) static void _add_following_entry(following_t*** list, int* count, following_t* add) { int index = tf_util_insert_index(add->id, *list, *count, sizeof(following_t*), _following_compare); - if (index >= *count || strcmp(add->id, (*list)[index]->id) == 0) + if (index >= *count || strcmp(add->id, (*list)[index]->id) != 0) { *list = tf_resize_vec(*list, sizeof(**list) * (*count + 1)); if (*count - index) @@ -1309,6 +1320,21 @@ static void _add_following_entry(following_t*** list, int* count, following_t* a memmove(*list + index + 1, *list + index, sizeof(following_t*) * (*count - index)); } (*list)[index] = add; + (*count)++; + } +} + +static void _remove_following_entry(following_t*** list, int* count, following_t* remove) +{ + int index = tf_util_insert_index(remove->id, *list, *count, sizeof(following_t*), _following_compare); + if (index < *count && strcmp(remove->id, (*list)[index]->id) == 0) + { + if (*count - index > 1) + { + memmove(*list + index, *list + index + 1, sizeof(following_t*) * (*count - index)); + } + *list = tf_resize_vec(*list, sizeof(**list) * (*count - 1)); + (*count)--; } } @@ -1354,21 +1380,31 @@ static following_t* _get_following(tf_ssb_t* ssb, const char* id, following_t*** const char* contact = (const char*)sqlite3_column_text(statement, 0); if (sqlite3_column_type(statement, 1) != SQLITE_NULL) { - bool is_following = sqlite3_column_int(statement, 1); + bool is_following = sqlite3_column_int(statement, 1) != 0; following_t* next = _get_following(ssb, contact, following, following_count, depth + 1, max_depth); if (is_following) { _add_following_entry(&entry->following, &entry->following_count, next); + next->ref_count++; + } + else + { + _remove_following_entry(&entry->following, &entry->following_count, next); + next->ref_count--; } } if (sqlite3_column_type(statement, 2) != SQLITE_NULL) { - bool is_blocking = sqlite3_column_int(statement, 2); + bool is_blocking = sqlite3_column_int(statement, 2 != 0); following_t* next = _get_following(ssb, contact, following, following_count, depth + 1, 0 /* don't dig deeper into blocked users */); if (is_blocking) { _add_following_entry(&entry->blocking, &entry->blocking_count, next); } + else + { + _remove_following_entry(&entry->blocking, &entry->blocking_count, next); + } } } } @@ -1385,18 +1421,33 @@ const char** tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, int count int following_count = 0; for (int i = 0; i < count; i++) { - _get_following(ssb, ids[i], &following, &following_count, 0, depth); + following_t* entry = _get_following(ssb, ids[i], &following, &following_count, 0, depth); + entry->ref_count++; } - char** result = tf_malloc(sizeof(char*) * (following_count + 1) + k_id_base64_len * following_count); - char* result_ids = (char*)result + sizeof(char*) * (following_count + 1); - + int actual_following_count = 0; for (int i = 0; i < following_count; i++) { - result[i] = result_ids + k_id_base64_len * i; - snprintf(result[i], k_id_base64_len, "%s", following[i]->id); + if (following[i]->ref_count > 0) + { + actual_following_count++; + } } - result[following_count] = NULL; + + char** result = tf_malloc(sizeof(char*) * (actual_following_count + 1) + k_id_base64_len * actual_following_count); + char* result_ids = (char*)result + sizeof(char*) * (actual_following_count + 1); + + int write_index = 0; + for (int i = 0; i < following_count; i++) + { + if (following[i]->ref_count > 0) + { + result[write_index] = result_ids + k_id_base64_len * write_index; + snprintf(result[write_index], k_id_base64_len, "%s", following[i]->id); + write_index++; + } + } + result[actual_following_count] = NULL; for (int i = 0; i < following_count; i++) { diff --git a/src/ssb.js.c b/src/ssb.js.c index a08dff6f..7163d84c 100644 --- a/src/ssb.js.c +++ b/src/ssb.js.c @@ -29,6 +29,7 @@ static const int k_sql_async_timeout_ms = 60 * 1000; static JSClassID _tf_ssb_classId; void _tf_ssb_on_rpc(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data); +static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv); static JSValue _tf_ssb_createIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) { @@ -63,6 +64,71 @@ static JSValue _tf_ssb_createIdentity(JSContext* context, JSValueConst this_val, return result; } +static JSValue _set_server_following_internal(tf_ssb_t* ssb, JSValueConst this_val, JSValue id, JSValue following) +{ + JSContext* context = tf_ssb_get_context(ssb); + JSValue message = JS_NewObject(context); + JSValue server_user = JS_NewString(context, ":admin"); + char server_id_buffer[k_id_base64_len] = { 0 }; + tf_ssb_whoami(ssb, server_id_buffer, sizeof(server_id_buffer)); + JSValue server_id = JS_NewString(context, server_id_buffer); + JS_SetPropertyStr(context, message, "type", JS_NewString(context, "contact")); + JS_SetPropertyStr(context, message, "contact", JS_DupValue(context, id)); + JS_SetPropertyStr(context, message, "following", JS_DupValue(context, following)); + JSValue args[] = + { + server_user, + server_id, + message, + }; + JSValue result = _tf_ssb_appendMessageWithIdentity(context, this_val, _countof(args), args); + JS_FreeValue(context, server_id); + JS_FreeValue(context, server_user); + JS_FreeValue(context, message); + return result; +} + +static JSValue _tf_ssb_set_server_following_me(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) +{ + tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId); + JSValue result = JS_UNDEFINED; + if (ssb) + { + char server_id[k_id_base64_len]; + tf_ssb_whoami(ssb, server_id, sizeof(server_id)); + const char* user = JS_ToCString(context, argv[0]); + const char* key = JS_ToCString(context, argv[1]); + if (!tf_ssb_db_identity_get_private_key(ssb, user, key, NULL, 0)) + { + result = JS_ThrowInternalError(context, "User %s does not own key %s.", user, key); + } + else + { + const char* server_id_ptr = server_id; + const char** current_following = tf_ssb_db_following_deep(ssb, &server_id_ptr, 1, 1); + bool is_following = false; + for (const char** it = current_following; *it; it++) + { + if (strcmp(key, *it) == 0) + { + is_following = true; + break; + } + } + tf_free(current_following); + + bool want_following = JS_ToBool(context, argv[2]); + if ((want_following && !is_following) || (!want_following && is_following)) + { + result = _set_server_following_internal(ssb, this_val, argv[1], argv[2]); + } + } + JS_FreeCString(context, key); + JS_FreeCString(context, user); + } + return result; +} + typedef struct _identities_visit_t { JSContext* context; @@ -1465,6 +1531,7 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb) /* Requires an identity. */ JS_SetPropertyStr(context, object, "createIdentity", JS_NewCFunction(context, _tf_ssb_createIdentity, "createIdentity", 1)); + JS_SetPropertyStr(context, object, "setServerFollowingMe", JS_NewCFunction(context, _tf_ssb_set_server_following_me, "setServerFollowingMe", 3)); JS_SetPropertyStr(context, object, "getIdentities", JS_NewCFunction(context, _tf_ssb_getIdentities, "getIdentities", 1)); JS_SetPropertyStr(context, object, "hmacsha256sign", JS_NewCFunction(context, _tf_ssb_hmacsha256_sign, "hmacsha256sign", 3)); JS_SetPropertyStr(context, object, "hmacsha256verify", JS_NewCFunction(context, _tf_ssb_hmacsha256_verify, "hmacsha256verify", 3));