diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h index bbe43622f..7a54439a7 100644 --- a/src/chat/chat_event.h +++ b/src/chat/chat_event.h @@ -98,6 +98,7 @@ extern "C" { #define CHAT_EVT_NETWORK_STATS 69 /* Native POD снимок ETCP-подключений и линков, chat_network_stats.h. */ #define CHAT_EVT_DM_START_RESULT 72 /* request/conv/generation:8 native + success:1; только результат конкретного запроса UI. */ #define CHAT_EVT_DM_MEMBERS 73 /* conv_len:1,conv,generation:LE8,count:LE2,(side:1,member:wire260)*count. */ +#define CHAT_EVT_GROUP_POLICY_RESULT 74 /* request/group:8LE + JSON: preview или результат смены прав приглашения. */ // ── Подключение получателя событий ── diff --git a/src/chat/group_policy.c b/src/chat/group_policy.c index 54f0968f1..bc4185024 100644 --- a/src/chat/group_policy.c +++ b/src/chat/group_policy.c @@ -28,7 +28,8 @@ static uint64_t gp_read64(const uint8_t* p) { uint64_t n = 0; for (int i = 0; i int group_policy_schema(sqlite3* db) { int rc = sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS group_policy(channel_id TEXT PRIMARY KEY," - "version INTEGER NOT NULL,all_members INTEGER NOT NULL,signature BLOB NOT NULL)", NULL, NULL, NULL); + "version INTEGER NOT NULL,all_members INTEGER NOT NULL,signature BLOB NOT NULL);" + "CREATE TABLE IF NOT EXISTS group_admission_pending(channel_id TEXT PRIMARY KEY)", NULL, NULL, NULL); if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "group policy schema: %s", sqlite3_errmsg(db)); return rc == SQLITE_OK ? 0 : -1; } @@ -199,9 +200,27 @@ static int gp_evaluate(struct gp_member* m, size_t count, int all, uint64_t owne } /* Изменение admitted не входит в Merkle: это производный допуск по уже сверенным данным. */ +int group_policy_pending(sqlite3* db, const char* channel) { + sqlite3_stmt* st = NULL; + int rc = sqlite3_prepare_v2(db, "SELECT 1 FROM group_admission_pending WHERE channel_id=?", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + int final = sqlite3_finalize(st); + if ((rc != SQLITE_ROW && rc != SQLITE_DONE) || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "group admission checkpoint read ch=%s rc=%d", channel, rc); return -1; + } + return rc == SQLITE_ROW; +} + int group_policy_refresh(struct UTUN_INSTANCE* inst, const char* channel) { sqlite3* db = inst ? inst->topo_sqlite_db : NULL; struct group_policy policy; struct gp_member* m = NULL; size_t count = 0; uint64_t owner; + if (!db) return -1; + int pending = group_policy_pending(db, channel); + if (pending) { + DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "group admission retains previous checkpoint ch=%s pending=%d", channel, pending); + return pending < 0 ? -1 : 0; + } if (!db || !chat_channel_id(channel) || !sqlite3_get_autocommit(db) || group_policy_get(db, channel, &policy) || topo_node_sqlite_channel_get(db, channel, NULL, 0, &owner, NULL, NULL, NULL) || gp_load(inst, channel, &m, &count)) return -1; if (gp_evaluate(m, count, policy.all_members, owner)) { u_free(m); return -1; } @@ -249,6 +268,9 @@ done: int group_policy_can_invite(struct UTUN_INSTANCE* inst, const char* channel, uint64_t inviter) { struct group_policy policy; struct gp_member* m = NULL; size_t count = 0; uint64_t owner; sqlite3* db = inst ? inst->topo_sqlite_db : NULL; + if (!db || group_policy_pending(db, channel)) { + DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "invitation waits for membership checkpoint ch=%s", channel); return 0; + } if (!db || !chat_channel_id(channel) || group_policy_get(db, channel, &policy) || topo_node_sqlite_channel_get(db, channel, NULL, 0, &owner, NULL, NULL, NULL) || gp_load(inst, channel, &m, &count)) return -1; int rc = gp_evaluate(m, count, policy.all_members, owner); @@ -294,6 +316,7 @@ int group_policy_command(struct UTUN_INSTANCE* inst, const char* channel, int al struct strbuf json = strbuf_new(); char expected[65]; int result = -1, transaction = 0; const char* error = "unavailable"; if (!output || !db || !chat_channel_id(channel) || (all != 0 && all != 1) || !sqlite3_get_autocommit(db)) goto done; + if (group_policy_pending(db, channel)) { error = "sync_pending"; goto done; } if (group_policy_get(db, channel, &policy) || topo_node_sqlite_channel_get(db, channel, NULL, 0, &owner, NULL, NULL, NULL) || merkle_sync_read_hash(inst, MT_DATA_MEMBERS, channel, 0, 0, root) || gp_load(inst, channel, &m, &count)) goto done; for (int i = 0; i < 32; i++) snprintf(expected + i * 2, 3, "%02x", root[i]); diff --git a/src/chat/group_policy.h b/src/chat/group_policy.h index c05f763e4..b2e7f5d7e 100644 --- a/src/chat/group_policy.h +++ b/src/chat/group_policy.h @@ -30,6 +30,8 @@ int group_policy_decode(struct sqlite3* db, const char* channel, const uint8_t* int group_policy_apply(struct sqlite3* db, const char* channel, const struct group_policy* policy); /* Пересчитать admitted атомарно, затем снять недопущенные сессии; 0/-1. */ int group_policy_refresh(struct UTUN_INSTANCE* inst, const char* channel); +/* Незавершённый приём страниц: 1/0/-1. Маркер хранится локально, не входит в Merkle. */ +int group_policy_pending(struct sqlite3* db, const char* channel); /* Текущее право подписи приглашения; 1/0/-1, ошибка никогда не разрешает действие. */ int group_policy_can_invite(struct UTUN_INSTANCE* inst, const char* channel, uint64_t inviter); /* Предпросмотр/применение: token=NULL — preview; иначе проверка Merkle token и commit. diff --git a/src/chat/member_sync.c b/src/chat/member_sync.c index 40e93fee3..d0e4fb364 100644 --- a/src/chat/member_sync.c +++ b/src/chat/member_sync.c @@ -1208,7 +1208,8 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, co if (sqlite3_exec(db, "SAVEPOINT member_page", NULL, NULL, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page begin: %s", MS_ID, sqlite3_errmsg(db)); u_free(changed); return MT_ERR_IO; } - int rc = group_policy_apply(db, ns, &policy); + char mark[128]; snprintf(mark, sizeof(mark), "INSERT OR IGNORE INTO group_admission_pending VALUES('%s')", ns); + int rc = sqlite3_exec(db, mark, NULL, NULL, NULL) == SQLITE_OK ? group_policy_apply(db, ns, &policy) : MT_ERR_IO; if (rc > 0) rc = merkle_sync_recompute_path(inst, MT_DATA_MEMBERS, ns, 0) < 0 ? MT_ERR_IO : 0; off = 2 + GROUP_POLICY_WIRE_SIZE; for (unsigned i = 0; i < count; i++) { @@ -1292,7 +1293,16 @@ static int member_finish(void* ctx, const char* ns) { static void member_policy_result(void* ctx, const char* ns, uint64_t peer, int result) { (void)peer; struct UTUN_INSTANCE* inst = ctx; - if (result != MT_OK || group_policy_refresh(inst, ns)) return; + if (result != MT_OK) return; + sqlite3_stmt* checkpoint = NULL; + int marked = sqlite3_prepare_v2(_db(inst), "DELETE FROM group_admission_pending WHERE channel_id=?", -1, &checkpoint, NULL); + if (marked == SQLITE_OK) marked = sqlite3_bind_text(checkpoint, 1, ns, -1, SQLITE_STATIC); + if (marked == SQLITE_OK) marked = sqlite3_step(checkpoint); + int finalized = sqlite3_finalize(checkpoint); + if (marked != SQLITE_DONE || finalized != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "group admission checkpoint commit ch=%s rc=%d", ns, marked); return; + } + if (group_policy_refresh(inst, ns)) return; struct ready_member { uint64_t node, signer; uint8_t x[32], signature[64]; }; struct ready_member* rows = NULL; size_t count = 0; char sql[192]; sqlite3_stmt* st = NULL;