diff --git a/src/dm/dm_core.c b/src/dm/dm_core.c index 8b61cff7..fb31d663 100644 --- a/src/dm/dm_core.c +++ b/src/dm/dm_core.c @@ -959,6 +959,18 @@ int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int c if (enc_len < DM_TAG_SIZE || enc_len > DM_DATA_MAX + DM_TAG_SIZE || dm_decrypt(c.content_key, nonce, enc, (size_t)enc_len, (uint8_t*)text, &plen)) goto fail; text[plen] = '\0'; + const char* group_name = ""; + int invite = ct && !strcmp(ct, "invite"); + if (invite) { + char* separator = memchr(text, '\0', plen); + if (!separator || (size_t)(separator - text) + 8 > plen || + strncmp(separator + 1, "utun://", 7) || memchr(separator + 1, '\0', plen - (separator + 1 - text))) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "invite PM: malformed payload conv=%s seq=%llu bytes=%zu", conv_id, + (unsigned long long)seq, plen); + goto fail; + } + group_name = text; + } struct attachment_info attachment = {0}; int file = ct && !strcmp(ct, "file"); if (file) { @@ -970,15 +982,19 @@ int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int c const char* path = ready == 1 ? (const char*)sqlite3_column_text(st, 10) : ""; const char* status = delivered && dir ? "delivered" : ready == 1 ? "ready" : downloading ? "downloading" : ready < 0 ? "expired" : "pending"; - char esc_ct[512], esc_data[DM_DATA_MAX * 2 + 8], esc_path[2048]; - json_escape(ct ? ct : "", esc_ct, sizeof(esc_ct)); json_escape(text, esc_data, sizeof(esc_data)); + char esc_ct[512], esc_data[DM_DATA_MAX * 6 + 1], esc_group[DM_DATA_MAX * 6 + 1], esc_path[2048]; + json_escape(group_name, esc_group, sizeof(esc_group)); + const char* display_data = invite ? text + strlen(group_name) + 1 : text; + json_escape(ct ? ct : "", esc_ct, sizeof(esc_ct)); json_escape(display_data, esc_data, sizeof(esc_data)); json_escape(path, esc_path, sizeof(esc_path)); if (strbuf_addf(&sb, "%s{\"dir\":%d,\"seq\":%llu,\"ts\":%llu,\"author\":\"%llu\",\"ct\":\"%s\",\"data\":\"%s\"," "\"pending\":%s,\"media_status\":\"%s\",\"media_path\":\"%s\",\"media_size\":%llu," + "\"tags\":{\"group_name\":\"%s\"}," "\"content_type\":\"%s\",\"duration_ms\":%u,\"width\":%u,\"height\":%u,\"waveform\":\"%s\"}", first ? "" : ",", dir, (unsigned long long)seq, (unsigned long long)ts, (unsigned long long)author, esc_ct, esc_data, sqlite3_column_int(st, 6) ? "true" : "false", status, esc_path, - (unsigned long long)sqlite3_column_int64(st, 11), file ? attachment_content_type(attachment.kind) : "text/plain", + (unsigned long long)sqlite3_column_int64(st, 11), esc_group, + file ? attachment_content_type(attachment.kind) : "text/plain", attachment.duration_ms, attachment.width, attachment.height, attachment.waveform) < 0) goto fail; first = 0; } @@ -1065,10 +1081,25 @@ 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)); size_t bytes = strlen(link); - DEBUG_INFO(DEBUG_CATEGORY_DM, "invite PM: registered request=%llu peer=%llu ch=%llu bytes=%zu ttl=%ds", + sqlite3_stmt* st = NULL; + char channel[64], payload[DM_DATA_MAX]; + snprintf(channel, sizeof(channel), "%llu", (unsigned long long)req->channel_id); + struct dm_state* dm = dm_of(req->inst); + int rc = sqlite3_prepare_v2(dm->db, "SELECT name FROM channels WHERE channel_id=?", -1, &st, NULL); + if (rc == SQLITE_OK) { sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); rc = sqlite3_step(st); } + size_t name_bytes = rc == SQLITE_ROW ? (size_t)sqlite3_column_bytes(st, 0) : 0; + if (rc != SQLITE_ROW || name_bytes + 1 + bytes > sizeof(payload)) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "invite PM: group metadata failed ch=%s rc=%d name_bytes=%zu link_bytes=%zu sql=%s", + channel, rc, name_bytes, bytes, sqlite3_errmsg(dm->db)); + sqlite3_finalize(st); dm_invite_done(req, DM_INVITE_SEND_ERROR); return; + } + memcpy(payload, sqlite3_column_text(st, 0), name_bytes); payload[name_bytes] = '\0'; + memcpy(payload + name_bytes + 1, link, bytes); + sqlite3_finalize(st); + DEBUG_INFO(DEBUG_CATEGORY_DM, "invite PM: registered request=%llu peer=%llu ch=%llu name_bytes=%zu link_bytes=%zu ttl=%ds", (unsigned long long)req->request_id, (unsigned long long)req->peer_node_id, - (unsigned long long)req->channel_id, bytes, JOIN_KEY_TTL_SECONDS); - int rc = dm_send(req->inst, conv, "text", (const uint8_t*)link, (uint32_t)bytes); + (unsigned long long)req->channel_id, name_bytes, bytes, JOIN_KEY_TTL_SECONDS); + rc = dm_send(req->inst, conv, "invite", (const uint8_t*)payload, (uint32_t)(name_bytes + 1 + bytes)); if (rc) DEBUG_ERROR(DEBUG_CATEGORY_DM, "invite PM: send failed request=%llu conv=%s rc=%d", (unsigned long long)req->request_id, conv, rc); dm_invite_done(req, rc ? DM_INVITE_SEND_ERROR : DM_INVITE_OK); diff --git a/src/dm/dm_core.h b/src/dm/dm_core.h index 33108e79..30c4f5f1 100644 --- a/src/dm/dm_core.h +++ b/src/dm/dm_core.h @@ -80,7 +80,9 @@ int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, /* Отправить сообщение в беседу. Шифрует, подписывает, сохраняет локально и * доставляет (прямое соединение при доступности пира, иначе mailbox). * conv_id — строковое представление (как в dm_conversations.conv_id). - * Для ct="file" data=[descriptor:56][basename:utf8], только после подготовки dm_media. */ + * Для ct="file" data=[descriptor:56][basename:utf8], только после подготовки dm_media. + * Для ct="invite" data=[group_name:utf8][NUL][полная ссылка:utf8], без завершающего NUL. + * Имя и ссылка шифруются и подписываются вместе; GUI получает data=ссылка, tags.group_name=имя. */ int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content_type, const uint8_t* data, uint32_t data_len); @@ -115,7 +117,7 @@ struct dm_send_req { void dm_send_trampoline(void* arg); /* Приглашение в выбранный канал через PM: свежая ссылка после ACK регистрации, - * затем обычный dm_send("text"). Получатель фиксирован независимо от текущего UI. + * затем dm_send("invite") с названием группы. Получатель фиксирован независимо от текущего UI. * Один ожидающий запрос на instance; результат CHAT_EVT_DM_INVITE_RESULT. * cancel=1 отменяет только ожидающий request_id; отправленное PM не отзывается. * arg=u_calloc(dm_invite_req); обработчик/финальный callback освобождает arg. */