You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

880 lines
38 KiB

/*
* chat_msg.c — сообщения: отправка, DB-операции для chat_sync
*
* Вынесено из chat_core.c для уменьшения размера модуля.
*/
#include "chat_core_priv.h"
#include "chat_event.h"
#include "chat_setting.h"
#include "chat_whisper.h"
#include "../utun_instance.h"
#include "../transport_layer/secure_channel.h"
#include "../media_delivery/media_index.h"
#include "../media_delivery/media_download.h"
#include "../video/video.h"
#include "../../lib/mem.h"
#include "../../lib/platform_compat.h"
#include <openssl/sha.h>
#include <stdlib.h>
chat_whisper_trigger_fn g_chat_whisper_trigger = NULL;
/* forward decl */
void chat_core_submit_media_message(struct chat_msg_submit* req);
static void wh_transcribe_done_cb(void* arg, const char* ch_id, const char* text, int err,
uint64_t reply_ts, uint64_t reply_node);
/* ─── отправка сообщения (GUI → uasync) ─── */
static int msg_get_prev_chain_hash(const char* tbl, uint8_t out[32]) {
char sql[128]; snprintf(sql, sizeof(sql),
"SELECT chain_hash FROM \"%s\" ORDER BY timestamp DESC LIMIT 1", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { memset(out,0,32); return -1; }
if (sqlite3_step(st) == SQLITE_ROW) {
const void* blob = sqlite3_column_blob(st, 0); int blen = sqlite3_column_bytes(st, 0);
if (blob && blen == 32) memcpy(out, blob, 32); else memset(out, 0, 32);
} else { memset(out, 0, 32); }
sqlite3_finalize(st); return 0;
}
void chat_core_submit_message(struct chat_msg_submit* req) {
if (!g_cc.initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: submit before init", CC_ID); return; }
if (!req) return;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: SUBMIT ch=%s ct=%s len=%u",
CC_ID, req->channel_id, req->content_type, req->data_len);
struct DB_SYNC_INSTANCE* si = si_find(req->channel_id);
if (!si) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: no db_sync instance for ch=%s", CC_ID, req->channel_id); return; }
char json[4096];
int off = snprintf(json, sizeof(json),
"{\"n\":%llu,\"ch\":\"%s\",\"ct\":\"%s\"",
(unsigned long long)g_cc.my_node_id, req->channel_id, req->content_type);
if (req->reply_to_ts) {
off += snprintf(json + off, sizeof(json) - off,
",\"rt\":%llu,\"rn\":%llu",
(unsigned long long)req->reply_to_ts, (unsigned long long)req->reply_to_node_id);
}
off += snprintf(json + off, sizeof(json) - off,
",\"d\":\"%.*s\"}",
(int)req->data_len, (const char*)req->data);
uint64_t ts = db_sync_next_timestamp(si);
uint8_t sig_msg[8192]; size_t soff = 0;
memcpy(sig_msg + soff, &ts, 8); soff += 8;
size_t jl = strlen(json); memcpy(sig_msg + soff, json, jl); soff += jl;
uint8_t sig[64];
if (sc_ed25519_sign(g_cc.inst->my_ed25519_privkey, sig_msg, soff, sig) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: Ed25519 sign failed", CC_ID);
return;
}
int ret = db_sync_insert_signed(si, json, strlen(json), sig, 64, ts, NULL);
if (ret != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: db_sync_insert_signed failed ret=%d", CC_ID, ret);
return;
}
}
void chat_core_submit_trampoline(void* arg) {
struct chat_msg_submit* req = (struct chat_msg_submit*)arg;
if (req->media_src[0] != '\0')
chat_core_submit_media_message(req);
else
chat_core_submit_message(req);
u_free(req);
}
/* ─── Медиа-сообщения: async регистрация в media_files + отправка ─── */
struct media_submit_ctx {
char channel_id[64];
char content_type[32];
char media_dest[1024];
uint8_t msg_data[4096];
uint32_t msg_data_len;
uint64_t timestamp;
int64_t duration_ms; /* видео: длительность после транскода */
int width; /* видео: размеры после транскода */
int height;
};
static void on_media_registered(void* arg, int err, const struct media_index_result* result) {
struct media_submit_ctx* mctx = (struct media_submit_ctx*)arg;
if (err || !result) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media registration failed ch=%s err=%d",
CC_ID, mctx->channel_id, err);
u_free(mctx);
return;
}
struct DB_SYNC_INSTANCE* si = si_find(mctx->channel_id);
if (!si) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: no db_sync instance for ch=%s", CC_ID, mctx->channel_id);
u_free(mctx);
return;
}
/* build full_data: <base>|<file_size>|<block_size>|<num_blocks>|<media_id_hex>|<content_hash_hex>|<id0,sig0,...> */
size_t base_len = mctx->msg_data_len;
int nb = result->num_blocks;
size_t pairs_len = (size_t)nb * (32 + 1 + 128 + 1); /* id_hex, sig_hex, commas */
size_t extra = 128 + 32 + 64 + pairs_len + 256;
size_t fdcap = base_len + extra;
char* full_data = u_malloc(fdcap);
if (!full_data) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: data alloc fail", CC_ID); u_free(mctx); return; }
size_t off = 0;
if (base_len > 0) {
char b64[2048]; size_t b64len = b64_encode(mctx->msg_data, base_len, b64, sizeof(b64));
full_data[off++] = '*';
memcpy(full_data + off, b64, b64len); off += b64len;
}
off += snprintf(full_data + off, fdcap - off, "|%lld|%lld|%d|",
(long long)result->file_size, (long long)result->block_size, nb);
for (int i = 0; i < 16; i++) off += snprintf(full_data + off, fdcap - off, "%02x", result->media_id[i]);
off += snprintf(full_data + off, fdcap - off, "|");
for (int i = 0; i < 32; i++) off += snprintf(full_data + off, fdcap - off, "%02x", result->content_hash[i]);
off += snprintf(full_data + off, fdcap - off, "|");
for (int n = 0; n < nb; n++) {
if (n > 0) full_data[off++] = ',';
for (int i = 0; i < 16; i++)
off += snprintf(full_data + off, fdcap - off, "%02x", result->block_ids[n * 16 + i]);
full_data[off++] = ',';
for (int i = 0; i < 64; i++)
off += snprintf(full_data + off, fdcap - off, "%02x", result->block_sigs[n * 64 + i]);
}
/* для видео дописываем длительность/размеры в конец (хвост игнорируется старыми парсерами) */
if (mctx->content_type[0] && strcmp(mctx->content_type, "video/mp4") == 0 && mctx->duration_ms > 0) {
off += snprintf(full_data + off, fdcap - off, "|dur=%lld|w=%d|h=%d",
(long long)mctx->duration_ms, mctx->width, mctx->height);
}
size_t full_data_len = off;
size_t json_cap = full_data_len + 256;
char* json = u_malloc(json_cap);
if (!json) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: json alloc fail", CC_ID); u_free(full_data); u_free(mctx); return; }
snprintf(json, json_cap,
"{\"n\":%llu,\"ch\":\"%s\",\"ct\":\"%s\",\"d\":\"%.*s\"}",
(unsigned long long)g_cc.my_node_id, mctx->channel_id,
mctx->content_type, (int)full_data_len, full_data);
uint64_t ts = db_sync_next_timestamp(si);
size_t sig_msg_len = 8 + strlen(json);
uint8_t* sig_msg = u_malloc(sig_msg_len);
if (!sig_msg) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: sig_msg alloc fail", CC_ID); u_free(json); u_free(full_data); u_free(mctx); return; }
memcpy(sig_msg, &ts, 8);
memcpy(sig_msg + 8, json, sig_msg_len - 8);
uint8_t json_sig[64];
if (sc_ed25519_sign(g_cc.inst->my_ed25519_privkey, sig_msg, sig_msg_len, json_sig) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: Ed25519 sign JSON failed", CC_ID);
u_free(sig_msg); u_free(json); u_free(full_data); u_free(mctx); return;
}
int ret = db_sync_insert_signed(si, json, strlen(json), json_sig, 64, ts, NULL);
if (ret != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media insert failed ret=%d", CC_ID, ret);
} else {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: media message inserted ch=%s ts=%llu",
CC_ID, mctx->channel_id, (unsigned long long)ts);
const char* base = strrchr(mctx->media_dest, '/');
const char* filename = base ? base + 1 : mctx->media_dest;
char attrs[1100]; snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", filename);
chat_core_update_local_attrs(mctx->channel_id, ts, json_sig, attrs);
/* whisper транскрипция для своих голосовых сообщений */
if (mctx->content_type[0] && strncmp(mctx->content_type, "voice", 5) == 0
&& g_cc.inst && g_cc.inst->media_async && g_chat_whisper_trigger) {
g_chat_whisper_trigger(g_cc.inst->media_async, g_cc.inst->ua,
mctx->media_dest, mctx->channel_id,
ts, g_cc.my_node_id,
wh_transcribe_done_cb, NULL);
}
}
u_free(sig_msg); u_free(json); u_free(full_data); u_free(mctx);
}
static void on_video_prepared(void* arg, int err) {
struct media_submit_ctx* mctx = (struct media_submit_ctx*)arg;
if (err) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: video prepare failed ch=%s err=%d",
CC_ID, mctx->channel_id, err);
u_free(mctx);
return;
}
/* после транскода знаем итоговую длительность и размеры — для тела сообщения */
struct video_info vi;
if (video_get_info(mctx->media_dest, &vi) == 0 && vi.is_video) {
mctx->duration_ms = vi.duration_ms;
mctx->width = vi.width;
mctx->height = vi.height;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: video prepared %dx%d dur=%lldms",
CC_ID, vi.width, vi.height, (long long)vi.duration_ms);
}
char media_base[1024];
{
const char* last_slash = strrchr(g_cc.db_path, '/');
if (last_slash)
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - g_cc.db_path), g_cc.db_path);
else
snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
}
media_index_register_async(
g_cc.inst->media_async, g_cc.inst->ua, g_cc.db,
g_cc.my_node_id, g_cc.inst->my_ed25519_privkey,
mctx->channel_id,
mctx->media_dest, mctx->media_dest, 0,
media_base,
on_media_registered, mctx);
}
void chat_core_submit_media_message(struct chat_msg_submit* req) {
if (!g_cc.initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media submit before init", CC_ID); return; }
if (!req || req->media_src[0] == '\0') return;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: MEDIA SUBMIT ch=%s ct=%s src=%s dst=%s copy=%d video=%d",
CC_ID, req->channel_id, req->content_type,
req->media_src, req->media_dest, req->media_copy, req->media_video);
if (!g_cc.inst->media_async) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media_async not initialized", CC_ID);
return;
}
/* build media_base from db_path: dirname(db_path) e.g. /path/to/data */
char media_base[1024];
{
const char* last_slash = strrchr(g_cc.db_path, '/');
if (last_slash)
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - g_cc.db_path), g_cc.db_path);
else
snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
}
/* copy fields for async callback (req is freed by trampoline immediately) */
struct media_submit_ctx* mctx = u_calloc(1, sizeof(*mctx));
if (!mctx) return;
snprintf(mctx->channel_id, sizeof(mctx->channel_id), "%s", req->channel_id);
snprintf(mctx->content_type, sizeof(mctx->content_type), "%s", req->content_type);
snprintf(mctx->media_dest, sizeof(mctx->media_dest), "%s", req->media_dest);
mctx->timestamp = req->timestamp;
if (req->data && req->data_len > 0) {
mctx->msg_data_len = req->data_len < sizeof(mctx->msg_data) ? req->data_len : sizeof(mctx->msg_data) - 1;
memcpy(mctx->msg_data, req->data, mctx->msg_data_len);
}
if (req->media_video) {
video_transcode_start(g_cc.inst->ua, req->media_src, req->media_dest,
on_video_prepared, mctx);
return;
}
media_index_register_async(
g_cc.inst->media_async, g_cc.inst->ua, g_cc.db,
g_cc.my_node_id, g_cc.inst->my_ed25519_privkey,
req->channel_id,
req->media_src, req->media_dest, req->media_copy ? 1 : 0,
media_base,
on_media_registered, mctx);
}
/* ─── Обновление local_attrs (для будущей приёмной стороны) ─── */
int chat_core_update_local_attrs(const char* ch_id, uint64_t ts,
const uint8_t* author_sig,
const char* local_attrs_json)
{
if (!g_cc.initialized || !ch_id || !author_sig || !local_attrs_json) return -1;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[256];
snprintf(sql, sizeof(sql),
"UPDATE \"%s\" SET local_attrs=? WHERE timestamp=? AND author_signature=?", tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: update local_attrs prep fail: %s", CC_ID, sqlite3_errmsg(g_cc.db));
return -1;
}
sqlite3_bind_text(stmt, 1, local_attrs_json, -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 2, (sqlite3_int64)ts);
sqlite3_bind_blob(stmt, 3, author_sig, DB_SIG_SIZE, SQLITE_STATIC);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: update local_attrs step fail rc=%d", CC_ID, rc);
return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: local_attrs updated ch=%s ts=%llu", CC_ID, ch_id, (unsigned long long)ts);
return 0;
}
/* ─── Отметка голосового как проигранного ─── */
void chat_core_mark_voice_played(const char* ch_id, uint64_t ts, uint64_t author_node_id) {
if (!g_cc.initialized || !ch_id || ts == 0) return;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[256];
snprintf(sql, sizeof(sql),
"SELECT local_attrs, author_signature FROM \"%s\" WHERE timestamp=? AND node_id=? LIMIT 1", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: mark_voice_played prep fail: %s", CC_ID, sqlite3_errmsg(g_cc.db));
return;
}
sqlite3_bind_int64(st, 1, (sqlite3_int64)ts);
sqlite3_bind_int64(st, 2, (sqlite3_int64)author_node_id);
if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); return; }
const char* la = (const char*)sqlite3_column_text(st, 0);
const uint8_t* sig = sqlite3_column_blob(st, 1);
int sig_len = sqlite3_column_bytes(st, 1);
if (!sig || sig_len != DB_SIG_SIZE) { sqlite3_finalize(st); return; }
uint8_t author_sig[DB_SIG_SIZE]; memcpy(author_sig, sig, DB_SIG_SIZE);
if (la && strstr(la, "\"pl\":1")) { sqlite3_finalize(st); return; } /* уже помечено */
/* вставляем "pl":1 перед последней '}' */
char new_attrs[256];
if (la && la[0]) {
size_t la_len = strlen(la);
if (la_len > 0 && la[la_len - 1] == '}') {
snprintf(new_attrs, sizeof(new_attrs), "%.*s,\"pl\":1}", (int)(la_len - 1), la);
} else {
snprintf(new_attrs, sizeof(new_attrs), "%s,\"pl\":1}", la);
}
} else {
snprintf(new_attrs, sizeof(new_attrs), "{\"pl\":1}");
}
sqlite3_finalize(st);
chat_core_update_local_attrs(ch_id, ts, author_sig, new_attrs);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: voice marked played ch=%s ts=%llu", CC_ID, ch_id, (unsigned long long)ts);
}
void chat_core_mark_voice_played_trampoline(void* arg) {
struct voice_played_req* req = (struct voice_played_req*)arg;
chat_core_mark_voice_played(req->channel_id, req->ts, req->author_node_id);
u_free(req);
}
/* ─── DB-операции для chat_sync ─── */
uint32_t chat_core_count(const char* ch_id) {
if (!g_cc.initialized) return 0;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[128]; snprintf(sql, sizeof(sql),
"SELECT COUNT(*) FROM \"%s\"", tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0;
uint32_t cnt = 0;
if (sqlite3_step(stmt) == SQLITE_ROW)
cnt = (uint32_t)sqlite3_column_int64(stmt, 0);
sqlite3_finalize(stmt);
return cnt;
}
int chat_core_chain_hash_at(const char* ch_id, uint32_t pos, uint8_t* hash_out) {
if (!g_cc.initialized || !hash_out) return -1;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[200]; snprintf(sql, sizeof(sql),
"SELECT chain_hash FROM \"%s\" ORDER BY timestamp, node_id ASC LIMIT 1 OFFSET ?",
tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) {
memset(hash_out, 0, 32); return -1;
}
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)pos);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const void* blob = sqlite3_column_blob(stmt, 0);
int len = sqlite3_column_bytes(stmt, 0);
if (blob && len == 32) memcpy(hash_out, blob, 32);
else memset(hash_out, 0, 32);
} else {
memset(hash_out, 0, 32);
}
sqlite3_finalize(stmt);
return 0;
}
int chat_core_list_channels(uint8_t* buf, size_t buf_size, size_t* out_len) {
if (!g_cc.initialized || !buf || !out_len) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db,
"SELECT channel_id FROM channels ORDER BY created_at ASC",
-1, &stmt, NULL) != SQLITE_OK) return -1;
uint8_t* out = buf;
uint8_t* start = out;
out += 2;
uint16_t cnt = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* ch_id = (const char*)sqlite3_column_text(stmt, 0);
int ch_len = sqlite3_column_bytes(stmt, 0);
if (!ch_id || ch_len <= 0 || ch_len > 63) continue;
size_t need = (size_t)(out - buf) + 1 + (size_t)ch_len;
if (need > buf_size) break;
*out++ = (uint8_t)ch_len;
memcpy(out, ch_id, (size_t)ch_len); out += ch_len;
cnt++;
}
sqlite3_finalize(stmt);
memcpy(start, &cnt, 2);
*out_len = (size_t)(out - buf);
return 0;
}
int chat_core_list_peers(const char* ch_id, uint8_t* buf, size_t buf_size,
size_t* out_len) {
if (!g_cc.initialized || !buf || !out_len) return -1;
char tbl[80]; peers_table_name(ch_id, tbl, sizeof(tbl));
char sql[128]; snprintf(sql, sizeof(sql),
"SELECT node_id FROM \"%s\"", tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) {
uint16_t z = 0; memcpy(buf, &z, 2); *out_len = 2; return -1;
}
uint16_t cnt = 0;
uint8_t* out = buf + 2;
while (sqlite3_step(stmt) == SQLITE_ROW) {
if ((size_t)(out - buf) + 8 > buf_size) break;
uint64_t nid = (uint64_t)sqlite3_column_int64(stmt, 0);
memcpy(out, &nid, 8); out += 8; cnt++;
}
sqlite3_finalize(stmt);
memcpy(buf, &cnt, 2);
*out_len = (size_t)(out - buf);
return 0;
}
int chat_core_load_nodeinfo(uint64_t node_id, uint8_t* buf, size_t buf_size,
size_t* out_len) {
if (!g_cc.initialized || !buf || !out_len) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db,
"SELECT x25519_pubkey, ed25519_pubkey FROM nodes WHERE node_id=?",
-1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; }
const uint8_t* x25519 = (const uint8_t*)sqlite3_column_blob(stmt, 0);
int x25519_len = sqlite3_column_bytes(stmt, 0);
const uint8_t* ed25519 = (const uint8_t*)sqlite3_column_blob(stmt, 1);
int ed25519_len = sqlite3_column_bytes(stmt, 1);
uint8_t* out = buf;
if (x25519 && x25519_len == 32) memcpy(out, x25519, 32);
else memset(out, 0, 32);
out += 32;
if (ed25519 && ed25519_len == 32) memcpy(out, ed25519, 32);
else memset(out, 0, 32);
out += 32;
sqlite3_finalize(stmt);
if (sqlite3_prepare_v2(g_cc.db,
"SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=?",
-1, &stmt, NULL) != SQLITE_OK) {
*out++ = 0; *out_len = (size_t)(out - buf); return 0;
}
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
uint8_t* cnt_pos = out; *out++ = 0;
uint8_t addr_cnt = 0;
while (sqlite3_step(stmt) == SQLITE_ROW && addr_cnt < 255) {
int family = sqlite3_column_int(stmt, 0);
int proto = sqlite3_column_int(stmt, 1);
const uint8_t* addr = (const uint8_t*)sqlite3_column_blob(stmt, 2);
int addr_len = sqlite3_column_bytes(stmt, 2);
uint16_t port = (uint16_t)sqlite3_column_int(stmt, 3);
int16_t rtt = (int16_t)sqlite3_column_int(stmt, 4);
if ((size_t)(out - buf) + 7 + (size_t)addr_len > buf_size) break;
*out++ = (uint8_t)family;
*out++ = (uint8_t)proto;
*out++ = (uint8_t)addr_len;
if (addr_len > 0) { memcpy(out, addr, (size_t)addr_len); out += addr_len; }
memcpy(out, &port, 2); out += 2;
memcpy(out, &rtt, 2); out += 2;
addr_cnt++;
}
*cnt_pos = addr_cnt;
sqlite3_finalize(stmt);
*out_len = (size_t)(out - buf);
return 0;
}
/* ─── download completion ─── */
struct md_done_ctx {
char channel_id[64];
uint64_t ts;
int64_t msg_id;
uint8_t author_sig[64];
char dest_relpath[512];
char dest_abs_path[1024];
char content_type[32];
uint64_t author_node_id;
};
static void md_download_progress_cb(void* arg, int blocks_done, int num_blocks) {
struct md_done_ctx* ctx = (struct md_done_ctx*)arg;
uint8_t evt[256];
uint8_t cl = (uint8_t)strlen(ctx->channel_id);
evt[0] = cl; memcpy(evt + 1, ctx->channel_id, cl);
memcpy(evt + 1 + cl, &ctx->msg_id, 8);
uint32_t bd = (uint32_t)blocks_done, nb = (uint32_t)num_blocks;
memcpy(evt + 1 + cl + 8, &bd, 4); memcpy(evt + 1 + cl + 12, &nb, 4);
chat_event_post(CHAT_EVT_DOWNLOAD_PROGRESS, evt, 1 + cl + 16);
}
static void md_download_done_cb(void* arg, int err) {
struct md_done_ctx* ctx = (struct md_done_ctx*)arg;
if (!err) {
char attrs[1024];
snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", ctx->dest_relpath);
chat_core_update_local_attrs(ctx->channel_id, ctx->ts, ctx->author_sig, attrs);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: media downloaded ch=%s", CC_ID, ctx->channel_id);
/* whisper транскрипция для голосовых сообщений */
if (ctx->content_type[0] && strncmp(ctx->content_type, "voice", 5) == 0
&& g_cc.inst && g_cc.inst->media_async && g_chat_whisper_trigger) {
g_chat_whisper_trigger(g_cc.inst->media_async, g_cc.inst->ua,
ctx->dest_abs_path, ctx->channel_id,
ctx->ts, ctx->author_node_id,
wh_transcribe_done_cb, NULL);
}
} else {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: media download failed ch=%s err=%d", CC_ID, ctx->channel_id, err);
/* пометить сообщение ошибкой, чтобы UI показал «повторить» */
char attrs[64];
snprintf(attrs, sizeof(attrs), "{\"st\":\"er\"}");
chat_core_update_local_attrs(ctx->channel_id, ctx->ts, ctx->author_sig, attrs);
}
uint8_t evt[80]; uint8_t cl = (uint8_t)strlen(ctx->channel_id);
evt[0] = cl; memcpy(evt + 1, ctx->channel_id, cl);
memcpy(evt + 1 + cl, &ctx->msg_id, 8);
chat_event_post(CHAT_EVT_ATTACHMENT_DOWNLOADED, evt, 1 + cl + 8);
u_free(ctx);
}
static void wh_transcribe_done_cb(void* arg, const char* ch_id, const char* text, int err,
uint64_t reply_ts, uint64_t reply_node) {
(void)arg;
if (err || !text || !text[0]) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: transcription failed ch=%s err=%d", CC_ID, ch_id, err);
return;
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: transcription result ch=%s: [%s]", CC_ID, ch_id, text);
/* mark original voice message as played */
chat_core_mark_voice_played(ch_id, reply_ts, reply_node);
struct chat_msg_submit* req = u_calloc(1, sizeof(*req));
if (!req) return;
snprintf(req->channel_id, sizeof(req->channel_id), "%s", ch_id);
snprintf(req->content_type, sizeof(req->content_type), "voice_transcription");
req->data = (uint8_t*)text;
req->data_len = (uint32_t)strlen(text);
req->reply_to_ts = reply_ts;
req->reply_to_node_id = reply_node;
chat_core_submit_message(req);
u_free(req);
}
static void sanitize_filename(char* buf, size_t size) {
char orig[256]; snprintf(orig, sizeof(orig), "%s", buf);
size_t w = 0;
for (size_t r = 0; buf[r]; r++) {
char c = buf[r];
if ((unsigned char)c < 0x20) continue;
if (c == '/' || c == '\\' || c == ':' || c == '*' || c == '?'
|| c == '"' || c == '<' || c == '>' || c == '|') continue;
if (w < size - 1) buf[w++] = c;
}
buf[w] = '\0';
if (w == 0) snprintf(buf, size, "file");
if (strcmp(orig, buf) != 0)
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: filename sanitized [%s] -> [%s]", CC_ID, orig, buf);
}
/* Извлекает имя файла из тела "d"-поля медиасообщения: берёт сегмент до '|',
декодирует base64url-префикс '*', если он есть, и санитизирует. */
static void extract_base_filename(const char* body, char* out, size_t out_sz) {
size_t bfi = 0;
while (body[bfi] && body[bfi] != '|' && bfi < out_sz - 1)
out[bfi++] = body[bfi];
out[bfi] = '\0';
if (out[0] == '*' && bfi > 1) {
uint8_t dec[256];
int dlen = (int)b64_decode(out + 1, bfi - 1, dec, sizeof(dec));
if (dlen > 0 && dlen < (int)out_sz) { memcpy(out, dec, (size_t)dlen); out[dlen] = '\0'; }
}
if (bfi == 0) snprintf(out, out_sz, "file");
sanitize_filename(out, out_sz);
}
static int md_start_download(struct UTUN_INSTANCE* inst,
const char* data_str, size_t data_len,
const char* ch_id, const char* base_filename,
uint64_t ts, const uint8_t* author_sig,
uint64_t author_node_id, int64_t msg_id,
const char* content_type) {
(void)data_len;
const char* p = data_str;
while (*p && *p != '|') p++;
if (!*p) return -1; p++;
long long fsize = strtoll(p, (char**)&p, 10);
if (*p != '|') return -1; p++;
long long bsize = strtoll(p, (char**)&p, 10);
if (*p != '|') return -1; p++;
int nb = (int)strtol(p, (char**)&p, 10);
while (*p && *p != '|') p++;
if (!*p) return -1; p++;
if (nb <= 0 || nb > 100 || fsize <= 0) return -1;
struct media_index_result result;
memset(&result, 0, sizeof(result));
result.file_size = fsize; result.block_size = bsize; result.num_blocks = nb;
for (int i = 0; i < 16; i++) {
char b[3] = { p[i*2], p[i*2+1], 0 };
result.media_id[i] = (uint8_t)strtol(b, NULL, 16);
}
p += 32; if (*p != '|') return -1; p++;
for (int i = 0; i < 32; i++) {
char b[3] = { p[i*2], p[i*2+1], 0 };
result.content_hash[i] = (uint8_t)strtol(b, NULL, 16);
}
p += 64; if (*p != '|') return -1; p++;
result.block_ids = u_malloc((size_t)nb * 16);
result.block_sigs = u_malloc((size_t)nb * 64);
if (!result.block_ids || !result.block_sigs) { media_index_result_free(&result); return -1; }
for (int n = 0; n < nb; n++) {
for (int i = 0; i < 16; i++) {
char b[3] = { p[i*2], p[i*2+1], 0 };
result.block_ids[n * 16 + i] = (uint8_t)strtol(b, NULL, 16);
}
p += 32; if (*p != ',') { media_index_result_free(&result); return -1; } p++;
for (int i = 0; i < 64; i++) {
char b[3] = { p[i*2], p[i*2+1], 0 };
result.block_sigs[n * 64 + i] = (uint8_t)strtol(b, NULL, 16);
}
p += 128; if (n < nb - 1 && *p == ',') p++;
}
/* для голосовых имя генерируем из media_id: msg_data — это wf-строка, а не имя файла */
char final_name[256];
if (content_type && strcmp(content_type, "audio/opus") == 0) {
char hex[33];
for (int i = 0; i < 16; i++) snprintf(hex + i * 2, 3, "%02x", result.media_id[i]);
snprintf(final_name, sizeof(final_name), "voice_%s.opus", hex);
} else {
snprintf(final_name, sizeof(final_name), "%s", base_filename);
}
char dest[1024], media_base[512];
{
const char* last_slash = strrchr(g_cc.db_path, '/');
if (last_slash) {
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - g_cc.db_path), g_cc.db_path);
} else {
snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
}
}
snprintf(dest, sizeof(dest), "%s/media/%s/%s", media_base, ch_id, final_name);
struct md_done_ctx* ctx = u_calloc(1, sizeof(*ctx));
if (!ctx) { media_index_result_free(&result); return -1; }
snprintf(ctx->channel_id, sizeof(ctx->channel_id), "%s", ch_id);
ctx->ts = ts; ctx->msg_id = msg_id;
snprintf(ctx->dest_relpath, sizeof(ctx->dest_relpath), "%s", final_name);
snprintf(ctx->dest_abs_path, sizeof(ctx->dest_abs_path), "%s", dest);
ctx->author_node_id = author_node_id;
if (content_type) snprintf(ctx->content_type, sizeof(ctx->content_type), "%s", content_type);
else ctx->content_type[0] = '\0';
if (author_sig) memcpy(ctx->author_sig, author_sig, 64);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: download start ch=%s ct=%s file=%s blocks=%d size=%lld dest=%s",
CC_ID, ch_id, content_type ? content_type : "?", final_name, nb, (long long)fsize, dest);
uint64_t gid = strtoull(ch_id, NULL, 10);
media_download_start(inst, gid, &result, dest, media_base, author_node_id, md_download_done_cb, ctx, md_download_progress_cb, ctx);
media_index_result_free(&result);
return 0;
}
static void md_auto_download(struct UTUN_INSTANCE* inst,
const char* data_str, size_t data_len,
const char* ch_id, const char* base_filename,
uint64_t ts, const uint8_t* author_sig,
uint64_t author, int64_t msg_id,
const char* content_type) {
if (!chat_setting_get_int("storage_autoload", 1)) return;
uint64_t max_size = g_cc.inst->config->global.chatserver_storage_unit_size;
const char* p = data_str;
while (*p && *p != '|') p++;
if (!*p) return; p++;
long long fsize = strtoll(p, (char**)&p, 10);
if (max_size > 0 && fsize > (long long)max_size) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: media too large (%lld > %llu bytes), skip auto-download",
CC_ID, fsize, (unsigned long long)max_size);
return;
}
md_start_download(inst, data_str, data_len, ch_id, base_filename, ts, author_sig, author, msg_id, content_type);
}
/* ─── db_sync callback ─── */
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg) {
(void)si;
const char* ch_id = (const char*)arg;
uint8_t evt[73]; uint8_t cl = (uint8_t)strlen(ch_id); evt[0] = cl; memcpy(evt + 1, ch_id, cl);
memcpy(evt + 1 + cl, &author, 8);
chat_event_post(CHAT_EVT_MSG_RECEIVED, evt, 1 + cl + 8);
/* auto-download media if message contains media metadata */
if (data && len > 0 && author != g_cc.my_node_id) {
/* data from db_sync may not be null-terminated; copy to local buffer */
char buf[4096]; size_t blen = len < sizeof(buf) - 1 ? len : sizeof(buf) - 1;
memcpy(buf, data, blen); buf[blen] = '\0';
const char* ct = strstr(buf, "\"ct\":\"");
const char* dpos = strstr(buf, "\"d\":\"");
if (ct && dpos) {
/* extract content_type value */
char content_type[32] = {0};
const char* ct_val = ct + 6;
int cti = 0;
while (ct_val[cti] && ct_val[cti] != '\"' && cti < (int)sizeof(content_type) - 1)
content_type[cti++] = ct_val[cti];
content_type[cti] = '\0';
const char* d_start = dpos + 5;
char body[4096]; size_t bi = 0;
while (*d_start && *d_start != '\"' && bi < sizeof(body) - 1) body[bi++] = *d_start++;
body[bi] = '\0';
char base_filename[256];
extract_base_filename(body, base_filename, sizeof(base_filename));
const uint8_t* sig_field = (const uint8_t*)strstr(buf, "\"sig\":\"");
uint8_t author_sig[64] = {0};
if (sig_field) {
/* read author_signature from DB — find by ts in on_msg_inserted we don't have sig
so leave as zeros; use record_ts to locate the record later */
}
/* We don't have author_sig in the callback; derive from body content.
Instead, read it from the DB using the just-inserted record */
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[256];
snprintf(sql, sizeof(sql), "SELECT author_signature, id FROM \"%s\" WHERE timestamp=? AND node_id=? LIMIT 1", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)record_ts);
sqlite3_bind_int64(st, 2, (sqlite3_int64)author);
int64_t msg_id = 0;
if (sqlite3_step(st) == SQLITE_ROW) {
const void* sblob = sqlite3_column_blob(st, 0);
int sblen = sqlite3_column_bytes(st, 0);
if (sblob && sblen == 64) memcpy(author_sig, sblob, 64);
msg_id = sqlite3_column_int64(st, 1);
}
sqlite3_finalize(st);
if (msg_id > 0) md_auto_download(g_cc.inst, body, bi, ch_id, base_filename, record_ts, author_sig, author, msg_id, content_type);
}
}
}
sqlite3_stmt* st = NULL;
sqlite3_prepare_v2(g_cc.db, "UPDATE channels SET last_msg_at = ? WHERE channel_id = ?", -1, &st, NULL);
if (st) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)record_ts);
sqlite3_bind_text(st, 2, ch_id, -1, SQLITE_STATIC);
sqlite3_step(st); sqlite3_finalize(st);
}
}
/* ─── manual attachment download (called from JNI bridge via uasync post) ─── */
void chat_core_attachment_download(const char* channel_id, int64_t msg_id) {
if (!channel_id || msg_id <= 0) return;
char tbl[80]; msg_table_name(channel_id, tbl, sizeof(tbl));
char sql[256];
snprintf(sql, sizeof(sql), "SELECT data, timestamp, author_signature, node_id FROM \"%s\" WHERE id=?", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: download sql prep failed ch=%s", CC_ID, channel_id);
return;
}
sqlite3_bind_int64(st, 1, (sqlite3_int64)msg_id);
if (sqlite3_step(st) != SQLITE_ROW) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: download msg not found ch=%s id=%lld", CC_ID, channel_id, (long long)msg_id);
sqlite3_finalize(st);
return;
}
const uint8_t* jdata = sqlite3_column_text(st, 0);
int jlen = sqlite3_column_bytes(st, 0);
int64_t db_ts = sqlite3_column_int64(st, 1);
const uint8_t* sig_blob = (const uint8_t*)sqlite3_column_blob(st, 2);
int sig_len = sqlite3_column_bytes(st, 2);
int64_t author_node_id = sqlite3_column_int64(st, 3);
if (!jdata || jlen <= 0 || !sig_blob || sig_len != 64) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: download bad msg data ch=%s id=%lld", CC_ID, channel_id, (long long)msg_id);
sqlite3_finalize(st);
return;
}
const char* ds = strstr((const char*)jdata, "\"d\":\"");
if (!ds) { sqlite3_finalize(st); return; }
const char* d_start = ds + 5;
char body[4096]; size_t bi = 0;
while (*d_start && *d_start != '\"' && bi < sizeof(body) - 1) body[bi++] = *d_start++;
body[bi] = '\0';
char content_type[32] = {0};
{
const char* ct = strstr((const char*)jdata, "\"ct\":\"");
if (ct) {
const char* cv = ct + 6;
int i = 0;
while (cv[i] && cv[i] != '\"' && i < (int)sizeof(content_type) - 1) content_type[i++] = cv[i];
content_type[i] = '\0';
}
}
char base_filename[256];
extract_base_filename(body, base_filename, sizeof(base_filename));
uint8_t author_sig[64]; memcpy(author_sig, sig_blob, 64);
sqlite3_finalize(st);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: manual download ch=%s id=%lld ct=%s file=%s",
CC_ID, channel_id, (long long)msg_id, content_type, base_filename);
md_start_download(g_cc.inst, body, bi, channel_id, base_filename, (uint64_t)db_ts, author_sig, (uint64_t)author_node_id, msg_id, content_type);
}
static void chat_core_attachment_download_trampoline_impl(void* arg) {
struct attachment_dl_req* req = (struct attachment_dl_req*)arg;
chat_core_attachment_download(req->channel_id, req->msg_id);
u_free(req);
}
void chat_core_attachment_download_trampoline(void* arg) {
chat_core_attachment_download_trampoline_impl(arg);
}