From 24e5baa8d444130eecdfb18716a3ea96d07c13b4 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 3 Nov 2026 14:10:16 +0100 Subject: [PATCH] Provide generation-bound DM device member snapshots --- src/chat/chat_event.h | 1 + src/dm/dm_core.c | 62 +++++++++++++++++++++++++++++++++++++++++++ src/dm/dm_core.h | 11 ++++++++ 3 files changed, 74 insertions(+) diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h index b61eee627..bbe43622f 100644 --- a/src/chat/chat_event.h +++ b/src/chat/chat_event.h @@ -97,6 +97,7 @@ extern "C" { #define CHAT_EVT_DEVGROUP_CHANGED 70 /* Обновление nodes/devgroup: перезагрузить снимки участников. */ #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. */ // ── Подключение получателя событий ── diff --git a/src/dm/dm_core.c b/src/dm/dm_core.c index 1b9624699..3bb488a9f 100644 --- a/src/dm/dm_core.c +++ b/src/dm/dm_core.c @@ -16,6 +16,8 @@ #include "dm_mailbox.h" #include "../chat/chat_core.h" #include "../chat/chat_event.h" +#include "../chat/chat_member.h" +#include "../member_files/protocol.h" #include "../chat/invite_build.h" #include "../chat/chat_join.h" #include "../utun_instance.h" @@ -1135,6 +1137,66 @@ static void json_escape(const char* src, char* dst, size_t dst_sz) { dst[i] = '\0'; } +int dm_members_snapshot(struct UTUN_INSTANCE* inst, const char* conv, uint64_t generation, uint8_t** out, size_t* size) { + struct dm_state* dm = dm_of(inst); struct dm_conv c; + if (out) *out = NULL; + if (size) *size = 0; + if (!dm || !conv || !conv[0] || strlen(conv) > 63 || !out || !size || !dm_history_current(inst, conv, generation)) { + DEBUG_WARN(DEBUG_CATEGORY_DM, "DM members: invalid/stale request conv=%s generation=%llu", conv ? conv : "", + (unsigned long long)generation); return -1; + } + if (dm_conv_load(dm, conv, &c) != DM_CONV_FOUND) return -1; + OPENSSL_cleanse(c.content_key, sizeof(c.content_key)); + struct dm_target members[DM_TARGET_MAX]; size_t count = 0; + if (!c.closed && c.mode == DM_MODE_GROUPS && dm_identity_members(inst, c.parties, members, DM_TARGET_MAX, &count)) return -1; + size_t cl = strlen(conv), bytes = 11 + cl + count * DM_MEMBER_WIRE_SIZE; + uint8_t* snapshot = u_calloc(1, bytes); + if (!snapshot) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM members: allocation failed count=%zu", count); return -1; } + snapshot[0] = (uint8_t)cl; memcpy(snapshot + 1, conv, cl); + mf_put64(snapshot + 1 + cl, generation); mf_put16(snapshot + 9 + cl, (uint16_t)count); + size_t pos = 11 + cl; + for (unsigned side = 0; side < 2; side++) for (size_t i = 0; i < count; i++) { + if (members[i].side != side) continue; + struct chat_member_display member = {0}; uint64_t group = 0; + if (!dm_find_route_group(inst, members[i].node, c.group_id, &group, 1)) dm_shared_group_id(dm, members[i].node, &group); + if (group) { + char channel[64]; snprintf(channel, sizeof(channel), "%llu", (unsigned long long)group); + uint8_t wire[CHAT_MEMBER_DISPLAY_SIZE]; + if (chat_core_get_single_member(inst, channel, members[i].node, wire) || chat_core_deserialize_member(wire, &member)) goto fail; + } else { + /* Узел может быть известен без общей CHAT-группы: он остаётся в списке как offline. */ + sqlite3_stmt* st = NULL; + int rc = sqlite3_prepare_v2(dm->db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)members[i].node); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + const char* name = rc == SQLITE_ROW ? (const char*)sqlite3_column_text(st, 0) : NULL; + if (name && name[0]) snprintf(member.name, sizeof(member.name), "%s", name); + else snprintf(member.name, sizeof(member.name), "0x%016llx", (unsigned long long)members[i].node); + int final = sqlite3_finalize(st); + if ((rc != SQLITE_ROW && rc != SQLITE_DONE) || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM members: node read failed rc=%d final=%d", rc, final); goto fail; + } + member.node_id = members[i].node; member.is_self = member.node_id == inst->node_id; + member.online = member.is_self; member.rtt = UINT16_MAX; member.nat_type = 255; + } + snapshot[pos++] = (uint8_t)side; chat_core_serialize_member(&member, snapshot + pos); pos += CHAT_MEMBER_DISPLAY_SIZE; + } + DEBUG_DEBUG(DEBUG_CATEGORY_DM, "DM members: snapshot conv=%s generation=%llu count=%zu closed=%d", conv, + (unsigned long long)generation, count, c.closed); + *out = snapshot; *size = bytes; return 0; +fail: + DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM members: snapshot failed conv=%s", conv); u_free(snapshot); return -1; +} + +void dm_members_trampoline(void* arg) { + struct dm_messages_req* req = arg; + if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM members: missing request"); return; } + uint8_t* snapshot = NULL; size_t size = 0; + if (!dm_members_snapshot(req->inst, req->conv_id, req->generation, &snapshot, &size)) + chat_event_post(req->inst, CHAT_EVT_DM_MEMBERS, snapshot, (int)size); + u_free(snapshot); u_free(req); +} + /* Список бесед в JSON. */ int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len) { struct dm_state* dm = dm_of(inst); diff --git a/src/dm/dm_core.h b/src/dm/dm_core.h index 4fd2d6d67..5a23d2c1d 100644 --- a/src/dm/dm_core.h +++ b/src/dm/dm_core.h @@ -24,6 +24,7 @@ #include #include "dm_media.h" #include "dm_protocol.h" +#include "../chat/chat_member.h" struct UTUN_INSTANCE; @@ -136,6 +137,16 @@ void dm_cancel_pending_history(struct UTUN_INSTANCE* inst, const char* conv); * Имя из БД либо nodes нормализуется по правилам dm_start; снимок не меняет сохранённые строки. */ int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len); +#define DM_MEMBER_WIRE_SIZE (1 + CHAT_MEMBER_DISPLAY_SIZE) +/* Только uasync. Текущие известные устройства закреплённых сторон GROUPS, включая offline. + * [conv_len:1][conv:UTF8][generation:LE64][count:LE16][side:1,member:wire260]*count. + * side=0 — свои,1 — собеседника. DEVICE/закрытая беседа возвращают пустой список. + * Требуется текущее ненулевое generation; при ошибке out=NULL,size=0; успех0, ошибка-1. + * Caller освобождает *out через u_free. Чтение не меняет снимки доставки сообщений. */ +int dm_members_snapshot(struct UTUN_INSTANCE* inst, const char* conv, uint64_t generation, uint8_t** out, size_t* size); +/* Забирает u_calloc(dm_messages_req), отправляет CHAT_EVT_DM_MEMBERS только при успехе. */ +void dm_members_trampoline(void* arg); + /* Сообщения беседы в JSON: расшифрованное содержимое, статусы доставки и медиа. * count=1..200 задаёт предел; count<=0 или count>200 означает 200. * offset>=0 отсчитывается от новых сообщений; offset=0 выбирает последние count.