diff --git a/src/ssb.db.c b/src/ssb.db.c index 82b540397..3efc6ad88 100644 --- a/src/ssb.db.c +++ b/src/ssb.db.c @@ -2164,7 +2164,6 @@ bool tf_ssb_db_generate_invite(sqlite3* db, const char* id, const char* host, in char public[k_id_base64_len]; tf_ssb_id_bin_to_str(public, sizeof(public), public_key); - tf_printf("invite is for public key %s\n", public); char seed_b64[64]; tf_base64_encode(seed, sizeof(seed), seed_b64, sizeof(seed_b64)); @@ -2188,3 +2187,22 @@ bool tf_ssb_db_generate_invite(sqlite3* db, const char* id, const char* host, in snprintf(out_invite, size, "%s:%d:%s~%s", host, port, id, seed_b64); return inserted; } + +bool tf_ssb_db_use_invite(sqlite3* db, const char* id) +{ + bool used = false; + sqlite3_stmt* statement; + if (sqlite3_prepare(db, + "UPDATE invites SET use_count = use_count - 1 WHERE invite_public_key = ? AND expires > ? AND (use_count > 0 OR use_count = -1)", + -1, &statement, NULL) == SQLITE_OK) + { + if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && + sqlite3_bind_int64(statement, 2, (int64_t)time(NULL)) == SQLITE_OK) + { + used = sqlite3_step(statement) == SQLITE_DONE && + sqlite3_changes(db) > 0; + } + sqlite3_finalize(statement); + } + return used; +} diff --git a/src/ssb.db.h b/src/ssb.db.h index b7984f5fb..0bea2100b 100644 --- a/src/ssb.db.h +++ b/src/ssb.db.h @@ -516,6 +516,14 @@ const char* tf_ssb_db_get_profile_name(sqlite3* db, const char* id); */ bool tf_ssb_db_generate_invite(sqlite3* db, const char* id, const char* host, int port, int use_count, int expires_seconds, char* out_invite, size_t size); +/** +** Consume and validate an invite. +** @param db The database. +** @param id The invite public key. +** @return true If the invite was valid and successfully consumed. +*/ +bool tf_ssb_db_use_invite(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.rpc.c b/src/ssb.rpc.c index 1140c5f8d..263c38227 100644 --- a/src/ssb.rpc.c +++ b/src/ssb.rpc.c @@ -1202,6 +1202,28 @@ static void _tf_ssb_rpc_ebt_replicate_server( tf_ssb_connection_add_request(connection, -request_number, "ebt.replicate", _tf_ssb_rpc_ebt_replicate, NULL, NULL, NULL); } +static void _tf_ssb_rpc_send_invite_use(tf_ssb_connection_t* connection) +{ + tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection); + JSContext* context = tf_ssb_get_context(ssb); + JSValue message = JS_NewObject(context); + JSValue name = JS_NewArray(context); + JS_SetPropertyUint32(context, name, 0, JS_NewString(context, "invite")); + JS_SetPropertyUint32(context, name, 1, JS_NewString(context, "use")); + JS_SetPropertyStr(context, message, "name", name); + JS_SetPropertyStr(context, message, "type", JS_NewString(context, "async")); + JSValue args = JS_NewArray(context); + JSValue object = JS_NewObject(context); + char id[k_id_base64_len] = { 0 }; + tf_ssb_whoami(ssb, id, sizeof(id)); + JS_SetPropertyStr(context, object, "feed", JS_NewString(context, id)); + JS_SetPropertyUint32(context, args, 0, object); + JS_SetPropertyStr(context, message, "args", args); + tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_new_request, tf_ssb_connection_next_request_number(connection), "invite.use", message, + _tf_ssb_rpc_connection_tunnel_isRoom_callback, NULL, NULL); + JS_FreeValue(context, message); +} + static void _tf_ssb_rpc_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_change_t change, tf_ssb_connection_t* connection, void* user_data) { JSContext* context = tf_ssb_get_context(ssb); @@ -1223,6 +1245,11 @@ static void _tf_ssb_rpc_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_chang if (tf_ssb_connection_is_client(connection)) { + if (tf_ssb_connection_get_flags(connection) & k_tf_ssb_connect_flag_use_invite) + { + _tf_ssb_rpc_send_invite_use(connection); + } + JSValue message = JS_NewObject(context); JSValue name = JS_NewArray(context); JS_SetPropertyUint32(context, name, 0, JS_NewString(context, "tunnel")); @@ -1608,11 +1635,6 @@ static void _tf_ssb_rpc_send_peers_exchange(tf_ssb_connection_t* connection) static void _tf_ssb_rpc_peers_exchange(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data) { tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection); - if (!tf_ssb_is_peer_exchange(ssb)) - { - tf_ssb_connection_rpc_send_error_method_not_allowed(connection, flags, -request_number, "peers.exchange"); - return; - } _tf_ssb_rpc_peers_exchange_internal(connection, flags, request_number, args, message, size, user_data); @@ -1624,6 +1646,114 @@ static void _tf_ssb_rpc_peers_exchange(tf_ssb_connection_t* connection, uint8_t JS_FreeValue(context, out_message); } +typedef struct _invite_t +{ + tf_ssb_connection_t* connection; + char pub[k_id_base64_len]; + char invite_public_key[k_id_base64_len]; + char id[k_id_base64_len]; + int32_t request_number; + bool accepted; + char previous_id[256]; + int64_t previous_sequence; + char* message; +} invite_t; + +static void _tf_ssb_rpc_invite_use_work(tf_ssb_connection_t* connection, void* user_data) +{ + invite_t* work = user_data; + tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection); + sqlite3* db = tf_ssb_acquire_db_writer(ssb); + work->accepted = tf_ssb_db_use_invite(db, work->invite_public_key); + tf_ssb_release_db_writer(ssb, db); + + if (work->accepted) + { + tf_ssb_db_get_latest_message_by_author(ssb, work->pub, &work->previous_sequence, work->previous_id, sizeof(work->previous_id)); + } +} + +static void _tf_ssb_invite_use_message_store_callback(const char* id, bool verified, bool is_new, void* user_data) +{ + invite_t* work = user_data; + tf_ssb_connection_t* connection = work->connection; + if (verified && is_new) + { + tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_json | k_ssb_rpc_flag_end_error, -work->request_number, NULL, + (const uint8_t*)work->message, strlen(work->message), NULL, NULL, NULL); + } + else + { + tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_json | k_ssb_rpc_flag_end_error, -work->request_number, NULL, + (const uint8_t*)"false", strlen("false"), NULL, NULL, NULL); + } + if (work->message) + { + tf_free(work->message); + } + tf_free(work); +} + +static void _tf_ssb_rpc_invite_use_after_work(tf_ssb_connection_t* connection, int status, void* user_data) +{ + invite_t* work = user_data; + if (work->accepted) + { + tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection); + JSContext* context = tf_ssb_connection_get_context(connection); + JSValue content = JS_NewObject(context); + JS_SetPropertyStr(context, content, "type", JS_NewString(context, "contact")); + JS_SetPropertyStr(context, content, "contact", JS_NewString(context, work->id)); + JS_SetPropertyStr(context, content, "following", JS_TRUE); + JS_SetPropertyStr(context, content, "pub", JS_TRUE); + + uint8_t private_key[512] = { 0 }; + tf_ssb_get_private_key(ssb, private_key, sizeof(private_key)); + + JSValue message = tf_ssb_sign_message(ssb, work->pub, private_key, content, work->previous_id, work->previous_sequence); + JSValue json = JS_JSONStringify(context, message, JS_NULL, JS_NULL); + const char* string = JS_ToCString(context, json); + work->message = tf_strdup(string); + JS_FreeCString(context, string); + JS_FreeValue(context, json); + tf_ssb_verify_strip_and_store_message(ssb, message, _tf_ssb_invite_use_message_store_callback, work); + JS_FreeValue(context, message); + JS_FreeValue(context, content); + } + else + { + tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_json | k_ssb_rpc_flag_end_error, -work->request_number, NULL, + (const uint8_t*)"false", strlen("false"), NULL, NULL, NULL); + } +} + +static void _tf_ssb_rpc_invite_use(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data) +{ + invite_t* work = tf_malloc(sizeof(invite_t)); + *work = (invite_t) + { + .connection = connection, + .request_number = request_number, + }; + tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection); + tf_ssb_whoami(ssb, work->pub, sizeof(work->pub)); + JSContext* context = tf_ssb_connection_get_context(connection); + JSValue array = JS_GetPropertyStr(context, args, "args"); + JSValue object = JS_GetPropertyUint32(context, array, 0); + JSValue feed = JS_GetPropertyStr(context, object, "feed"); + tf_ssb_connection_get_id(connection, work->invite_public_key, sizeof(work->invite_public_key)); + const char* id = JS_ToCString(context, feed); + snprintf(work->id, sizeof(work->id), "%s", id); + tf_printf("PUB = %s\n", work->pub); + tf_printf("FEED = %s\n", work->id); + tf_printf("INVITE_PUBLIC_KEY = %s\n", work->invite_public_key); + JS_FreeCString(context, id); + JS_FreeValue(context, feed); + JS_FreeValue(context, object); + JS_FreeValue(context, array); + tf_ssb_connection_run_work(connection, _tf_ssb_rpc_invite_use_work, _tf_ssb_rpc_invite_use_after_work, work); +} + void tf_ssb_rpc_register(tf_ssb_t* ssb) { tf_ssb_add_connections_changed_callback(ssb, _tf_ssb_rpc_connections_changed_callback, NULL, NULL); @@ -1640,4 +1770,5 @@ void tf_ssb_rpc_register(tf_ssb_t* ssb) tf_ssb_add_rpc_callback(ssb, "createHistoryStream", _tf_ssb_rpc_createHistoryStream, NULL, NULL); /* SOURCE */ tf_ssb_add_rpc_callback(ssb, "ebt.replicate", _tf_ssb_rpc_ebt_replicate_server, NULL, NULL); /* DUPLEX */ tf_ssb_add_rpc_callback(ssb, "peers.exchange", _tf_ssb_rpc_peers_exchange, NULL, NULL); /* ASYNC */ + tf_ssb_add_rpc_callback(ssb, "invite.use", _tf_ssb_rpc_invite_use, NULL, NULL); /* ASYNC */ }