feat(chat): add local DM deletion and improve Android member list
Delete local conversations atomically, retain replay protection and sequence counters, and guard asynchronous media work by history generation. Add Android avatar menu and confirmation, persistent file cleanup, and regression coverage. Fix unnamed member wire padding and compact Android member action spacing. Validation: 158 core tests passed; targeted DM tests, desktop build, Android unit tests and debug APK build passed.
This commit is contained in:
1 parent
97710b609f
commit
be295f768c
38 files changed
+1005
-126
No files matched your search
+58
-11
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "dm_core.h"
|
||||
#include "dm_crypto.h"
|
||||
#include "dm_history.h"
|
||||
#include "dm_mailbox.h"
|
||||
#include "../chat/chat_core.h"
|
||||
#include "../chat/chat_event.h"
|
||||
@@ -450,12 +451,15 @@ int dm_accept_message(struct UTUN_INSTANCE* inst, const uint8_t* msg, size_t len
|
||||
return -1;
|
||||
}
|
||||
if (dm_verify_message(inst, inst->node_id, msg, len) != 0) return -1;
|
||||
int deleted = dm_history_deleted(inst, msg, len, 0, NULL);
|
||||
if (deleted < 0) return -1;
|
||||
uint64_t conv, seq, author;
|
||||
memcpy(&conv, msg, 8);
|
||||
memcpy(&seq, msg + 8, 8);
|
||||
memcpy(&author, msg + 24, 8);
|
||||
char conv_id[64];
|
||||
snprintf(conv_id, sizeof(conv_id), "%llu", (unsigned long long)conv);
|
||||
if (deleted) goto receipt;
|
||||
struct dm_conv c;
|
||||
int created = dm_conv_load(dm, conv_id, &c) != 0;
|
||||
if (created) {
|
||||
@@ -490,10 +494,12 @@ int dm_accept_message(struct UTUN_INSTANCE* inst, const uint8_t* msg, size_t len
|
||||
uint64_t ts;
|
||||
memcpy(&ts, msg + 16, 8);
|
||||
if (ts > c.last_ts) c.last_ts = ts;
|
||||
if (inserted < 0 || dm_conv_save(dm, &c) != 0 || dm_media_track(inst, 0, msg, len) != 0 || dm_exec(dm, "COMMIT") != 0) {
|
||||
if (inserted < 0 || dm_conv_save(dm, &c) != 0 || dm_media_track(inst, 0, msg, len) != 0 ||
|
||||
(inserted && dm_history_show(inst, conv_id)) || dm_exec(dm, "COMMIT") != 0) {
|
||||
dm_exec(dm, "ROLLBACK");
|
||||
return -1;
|
||||
}
|
||||
receipt:
|
||||
memcpy(receipt, "DMACK001", 8);
|
||||
memcpy(receipt + 8, msg, 16);
|
||||
memcpy(receipt + 24, &author, 8);
|
||||
@@ -503,6 +509,7 @@ int dm_accept_message(struct UTUN_INSTANCE* inst, const uint8_t* msg, size_t len
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: receipt signing failed conv=%s seq=%llu", DM_ID, conv_id, (unsigned long long)seq);
|
||||
return -1;
|
||||
}
|
||||
if (deleted) return 0;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DM, "%s: committed receive conv=%s seq=%llu new=%d plain=%zu", DM_ID,
|
||||
conv_id, (unsigned long long)seq, inserted, plain_len);
|
||||
if (inserted) {
|
||||
@@ -511,7 +518,12 @@ int dm_accept_message(struct UTUN_INSTANCE* inst, const uint8_t* msg, size_t len
|
||||
evt[0] = (uint8_t)cl;
|
||||
memcpy(evt + 1, conv_id, cl);
|
||||
memcpy(evt + 1 + cl, &author, 8);
|
||||
if (created) chat_event_post(inst, CHAT_EVT_DM_CONV_UPDATED, evt, 1 + (int)cl);
|
||||
if (created) {
|
||||
uint64_t generation = dm_history_generation(inst, conv_id);
|
||||
memcpy(evt + 1 + cl, &generation, 8);
|
||||
chat_event_post(inst, CHAT_EVT_DM_CONV_UPDATED, evt, 1 + (int)cl + 8);
|
||||
memcpy(evt + 1 + cl, &author, 8);
|
||||
}
|
||||
chat_event_post(inst, CHAT_EVT_DM_MSG_RECEIVED, evt, 1 + (int)cl + 8);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: received conv=%s seq=%llu author=%llu", DM_ID, conv_id,
|
||||
(unsigned long long)seq, (unsigned long long)author);
|
||||
@@ -739,13 +751,17 @@ int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
|
||||
(unsigned long long)c.group_id);
|
||||
}
|
||||
|
||||
if (dm_history_show(inst, conv_id)) return -1;
|
||||
|
||||
/* событие GUI (всегда, чтобы GUI мог открыть/выделить беседу) */
|
||||
{
|
||||
size_t cl = strlen(conv_id);
|
||||
uint8_t evt[1 + 64];
|
||||
uint8_t evt[1 + 64 + 8];
|
||||
evt[0] = (uint8_t)cl;
|
||||
memcpy(evt + 1, conv_id, cl);
|
||||
chat_event_post(inst, CHAT_EVT_DM_CONV_UPDATED, evt, 1 + (int)cl);
|
||||
uint64_t generation = dm_history_generation(inst, conv_id);
|
||||
memcpy(evt + 1 + cl, &generation, 8);
|
||||
chat_event_post(inst, CHAT_EVT_DM_CONV_UPDATED, evt, 1 + (int)cl + 8);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -763,6 +779,9 @@ int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: send conversation missing/seq exhausted conv=%s", DM_ID, conv_id);
|
||||
return -1;
|
||||
}
|
||||
if (!dm_history_current(inst, conv_id, dm_history_generation(inst, conv_id))) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DM, "DM send: hidden conversation conv=%s", conv_id); return -1;
|
||||
}
|
||||
uint64_t seq = c.last_out_seq + 1;
|
||||
uint64_t ts = (uint64_t)(ntp_time_get_us(inst) / 1000);
|
||||
uint8_t* body = NULL;
|
||||
@@ -815,7 +834,7 @@ int dm_core_init(struct UTUN_INSTANCE* inst) {
|
||||
inst->dm = dm;
|
||||
dm->inst = inst;
|
||||
dm->db = inst->topo_sqlite_db;
|
||||
if (!dm->db || dm_create_tables(dm) != 0) goto fail;
|
||||
if (!dm->db || dm_create_tables(dm) != 0 || dm_history_init(inst)) goto fail;
|
||||
if (etcp_router_bind(inst, ETCP_RT_ID_DM, dm_recv_cb) != 0) goto fail;
|
||||
dm->initialized = 1;
|
||||
dm->timer = uasync_set_timeout(inst->ua, 10000, dm, dm_tick, "dm_outbox");
|
||||
@@ -877,8 +896,9 @@ int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf
|
||||
int failed = strbuf_addf(&json, "[") < 0;
|
||||
sqlite3_stmt* st = NULL;
|
||||
if (sqlite3_prepare_v2(dm->db,
|
||||
"SELECT conv_id, peer_node_id, COALESCE(peer_name,''), last_ts, last_out_seq, last_in_seq"
|
||||
" FROM dm_conversations ORDER BY last_ts DESC LIMIT 200",
|
||||
"SELECT c.conv_id,peer_node_id,COALESCE(peer_name,''),last_ts,last_out_seq,last_in_seq,COALESCE(h.generation,1)"
|
||||
" FROM dm_conversations c LEFT JOIN dm_history h USING(conv_id) WHERE COALESCE(h.hidden,0)=0"
|
||||
" ORDER BY last_ts DESC LIMIT 200",
|
||||
-1, &st, NULL) == SQLITE_OK) {
|
||||
int first = 1;
|
||||
while (sqlite3_step(st) == SQLITE_ROW) {
|
||||
@@ -898,10 +918,11 @@ int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf
|
||||
json_escape(name ? name : "", esc_name, sizeof(esc_name));
|
||||
if (strbuf_addf(&json,
|
||||
"%s{\"conv_id\":\"%s\",\"peer_node_id\":%llu,\"peer_name\":\"%s\","
|
||||
"\"last_ts\":%llu,\"out_seq\":%llu,\"in_seq\":%llu}",
|
||||
"\"last_ts\":%llu,\"out_seq\":%llu,\"in_seq\":%llu,\"generation\":%llu}",
|
||||
first ? "" : ",", conv_id, (unsigned long long)peer,
|
||||
esc_name, (unsigned long long)last_ts,
|
||||
(unsigned long long)out_seq, (unsigned long long)in_seq) < 0) { failed = 1; break; }
|
||||
(unsigned long long)out_seq, (unsigned long long)in_seq,
|
||||
(unsigned long long)sqlite3_column_int64(st, 6)) < 0) { failed = 1; break; }
|
||||
first = 0;
|
||||
}
|
||||
if (sqlite3_errcode(dm->db) != SQLITE_DONE && sqlite3_errcode(dm->db) != SQLITE_OK) failed = 1;
|
||||
@@ -1045,7 +1066,8 @@ void dm_send_trampoline(void* arg) {
|
||||
if (!req) return;
|
||||
struct UTUN_INSTANCE* inst = req->inst;
|
||||
char conv[64]; strncpy(conv, req->conv_id, sizeof(conv) - 1); conv[sizeof(conv) - 1] = '\0';
|
||||
int rc = dm_send(inst, conv, "text", req->data, req->data_len);
|
||||
uint64_t generation = req->generation ? req->generation : dm_history_generation(inst, conv);
|
||||
int rc = dm_history_current(inst, conv, generation) ? dm_send(inst, conv, "text", req->data, req->data_len) : -1;
|
||||
u_free(arg);
|
||||
if (rc != 0)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: dm_send_trampoline — dm_send failed conv=%s",
|
||||
@@ -1058,9 +1080,11 @@ static void dm_invite_done(struct dm_invite_req* req, int result) {
|
||||
if (dm && dm->invite == req) dm->invite = NULL;
|
||||
uint64_t conv = dm_derive_conv_id(req->inst->node_id, req->peer_node_id);
|
||||
int32_t status = result;
|
||||
uint8_t event[28];
|
||||
uint8_t event[36];
|
||||
memcpy(event, &req->request_id, 8); memcpy(event + 8, &status, 4);
|
||||
memcpy(event + 12, &req->peer_node_id, 8); memcpy(event + 20, &conv, 8);
|
||||
char id[64]; snprintf(id, sizeof(id), "%llu", (unsigned long long)conv);
|
||||
uint64_t generation = dm_history_generation(req->inst, id); memcpy(event + 28, &generation, 8);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "invite PM: finished request=%llu peer=%llu ch=%llu result=%d",
|
||||
(unsigned long long)req->request_id, (unsigned long long)req->peer_node_id,
|
||||
(unsigned long long)req->channel_id, result);
|
||||
@@ -1080,6 +1104,7 @@ static void dm_invite_ready(void* arg, int result, const char* link) {
|
||||
}
|
||||
char conv[64];
|
||||
snprintf(conv, sizeof(conv), "%llu", (unsigned long long)dm_derive_conv_id(req->inst->node_id, req->peer_node_id));
|
||||
if (!dm_history_current(req->inst, conv, req->generation)) { dm_invite_done(req, DM_INVITE_CANCELLED); return; }
|
||||
size_t bytes = strlen(link);
|
||||
sqlite3_stmt* st = NULL;
|
||||
char channel[64], payload[DM_DATA_MAX];
|
||||
@@ -1147,6 +1172,9 @@ void dm_invite_trampoline(void* arg) {
|
||||
dm_invite_done(req, DM_INVITE_START_ERROR); return;
|
||||
}
|
||||
}
|
||||
if (dm_history_show(req->inst, conv)) { dm_invite_done(req, DM_INVITE_START_ERROR); return; }
|
||||
req->generation = dm_history_generation(req->inst, conv);
|
||||
if (!dm_history_current(req->inst, conv, req->generation)) { dm_invite_done(req, DM_INVITE_CANCELLED); return; }
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "invite PM: building request=%llu peer=%llu source=%s ch=%s conv=%s",
|
||||
(unsigned long long)req->request_id, (unsigned long long)req->peer_node_id, req->source_ch_id, channel, conv);
|
||||
dm->invite = req;
|
||||
@@ -1154,6 +1182,25 @@ void dm_invite_trampoline(void* arg) {
|
||||
if (!req->pending) dm_invite_ready(req, CHAT_JOIN_ERROR, NULL);
|
||||
}
|
||||
|
||||
void dm_cancel_pending_history(struct UTUN_INSTANCE* inst, const char* conv) {
|
||||
struct dm_state* dm = dm_of(inst);
|
||||
if (dm && dm->invite && dm_derive_conv_id(inst->node_id, dm->invite->peer_node_id) == strtoull(conv, NULL, 10))
|
||||
chat_invite_build_cancel(dm->invite->pending);
|
||||
}
|
||||
|
||||
void dm_delete_trampoline(void* arg) {
|
||||
struct dm_delete_req* req = arg;
|
||||
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM delete: missing request"); return; }
|
||||
int success = dm_history_delete(req->inst, req->conv_id, req->generation) == 0;
|
||||
uint64_t conv = strtoull(req->conv_id, NULL, 10);
|
||||
uint8_t event[25];
|
||||
memcpy(event, &req->request_id, 8); memcpy(event + 8, &conv, 8); memcpy(event + 16, &req->generation, 8); event[24] = success;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "DM delete result: request=%llu conv=%s generation=%llu success=%d",
|
||||
(unsigned long long)req->request_id, req->conv_id, (unsigned long long)req->generation, success);
|
||||
chat_event_post(req->inst, CHAT_EVT_DM_DELETE_RESULT, event, sizeof(event));
|
||||
u_free(req);
|
||||
}
|
||||
|
||||
/* Единая BGP-проверка для кнопки PM и запуска звонка; только uasync-поток. */
|
||||
int dm_call_route(struct UTUN_INSTANCE* inst, const char* conv_id, uint64_t* peer, uint64_t* group_id) {
|
||||
struct dm_state* dm = dm_of(inst);
|
||||
|
||||
Reference in new issue
Block a user