|
|
|
|
@ -6,6 +6,7 @@
|
|
|
|
|
|
|
|
|
|
#include "chat_core_priv.h" |
|
|
|
|
#include "chat_event.h" |
|
|
|
|
#include "chat_setting.h" |
|
|
|
|
|
|
|
|
|
#include "../utun_instance.h" |
|
|
|
|
#include "../transport_layer/secure_channel.h" |
|
|
|
|
@ -384,94 +385,129 @@ int chat_core_load_nodeinfo(uint64_t node_id, uint8_t* buf, size_t buf_size,
|
|
|
|
|
return 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ─── db_sync callback ─── */ |
|
|
|
|
/* ─── download completion ─── */ |
|
|
|
|
|
|
|
|
|
struct md_done_ctx { |
|
|
|
|
char channel_id[64]; |
|
|
|
|
uint64_t ts; |
|
|
|
|
uint8_t author_sig[64]; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
static void md_download_done_cb(void* arg, int err) { |
|
|
|
|
struct md_done_ctx* ctx = (struct md_done_ctx*)arg; |
|
|
|
|
if (!err) { |
|
|
|
|
char dest_relpath[1024]; |
|
|
|
|
{ |
|
|
|
|
const char* last_slash = strrchr(g_cc.db_path, '/'); |
|
|
|
|
const char* base = last_slash ? last_slash + 1 : g_cc.db_path; |
|
|
|
|
snprintf(dest_relpath, sizeof(dest_relpath), "media/%s", base); |
|
|
|
|
} |
|
|
|
|
char attrs[1024]; |
|
|
|
|
snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", 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); |
|
|
|
|
} else { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: media download failed ch=%s err=%d", CC_ID, ctx->channel_id, err); |
|
|
|
|
} |
|
|
|
|
uint8_t evt[73]; uint8_t cl = (uint8_t)strlen(ctx->channel_id); |
|
|
|
|
evt[0] = cl; memcpy(evt + 1, ctx->channel_id, cl); { uint64_t z = 0; memcpy(evt + 1 + cl, &z, 8); } |
|
|
|
|
chat_event_post(CHAT_EVT_ATTACHMENT_DOWNLOADED, evt, 1 + cl + 8); |
|
|
|
|
u_free(ctx); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static void md_auto_download(struct UTUN_INSTANCE* inst, const char* data_str, size_t data_len, |
|
|
|
|
const char* ch_id, const char* media_base) { |
|
|
|
|
/* parse: <base>|<file_size>|<block_size>|<num_blocks>|<media_id_hex>|<content_hash_hex>|<ids,sigs> */ |
|
|
|
|
/* find first | after base (strip base prefix) */ |
|
|
|
|
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) { |
|
|
|
|
(void)data_len; |
|
|
|
|
const char* p = data_str; |
|
|
|
|
while (*p && *p != '|') p++; |
|
|
|
|
if (!*p) return; /* not a media message */ |
|
|
|
|
p++; /* skip first | */ |
|
|
|
|
|
|
|
|
|
if (!*p) return -1; p++; |
|
|
|
|
long long fsize = strtoll(p, (char**)&p, 10); |
|
|
|
|
if (*p != '|') return; p++; |
|
|
|
|
if (*p != '|') return -1; p++; |
|
|
|
|
long long bsize = strtoll(p, (char**)&p, 10); |
|
|
|
|
if (*p != '|') return; p++; |
|
|
|
|
int nb = atoi(p); |
|
|
|
|
if (*p != '|') return -1; p++; |
|
|
|
|
int nb = (int)strtol(p, (char**)&p, 10); |
|
|
|
|
while (*p && *p != '|') p++; |
|
|
|
|
if (!*p) return; p++; |
|
|
|
|
|
|
|
|
|
if (nb <= 0 || nb > 100) return; |
|
|
|
|
if (fsize <= 0) return; |
|
|
|
|
|
|
|
|
|
/* check auto-download settings */ |
|
|
|
|
char sql[128]; |
|
|
|
|
snprintf(sql, sizeof(sql), "SELECT value FROM ui_state WHERE key='auto_download_max_size'"); |
|
|
|
|
sqlite3_stmt* st = NULL; |
|
|
|
|
long long max_size = 0; |
|
|
|
|
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) { |
|
|
|
|
if (sqlite3_step(st) == SQLITE_ROW) max_size = strtoll((const char*)sqlite3_column_text(st, 0), NULL, 10); |
|
|
|
|
sqlite3_finalize(st); |
|
|
|
|
} |
|
|
|
|
if (max_size > 0 && fsize > max_size) { |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_DB_SYNC, "%s: media too large (%lld > %lld), skip auto-download", CC_ID, fsize, max_size); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
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; |
|
|
|
|
result.file_size = fsize; result.block_size = bsize; result.num_blocks = nb; |
|
|
|
|
|
|
|
|
|
/* media_id hex → bin */ |
|
|
|
|
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; p++; |
|
|
|
|
|
|
|
|
|
/* content_hash hex → bin */ |
|
|
|
|
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; p++; |
|
|
|
|
p += 64; if (*p != '|') return -1; p++; |
|
|
|
|
|
|
|
|
|
/* block_ids and block_sigs */ |
|
|
|
|
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; } |
|
|
|
|
|
|
|
|
|
if (!result.block_ids || !result.block_sigs) { media_index_result_free(&result); return -1; } |
|
|
|
|
for (int n = 0; n < nb; n++) { |
|
|
|
|
/* block_id hex */ |
|
|
|
|
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; } |
|
|
|
|
p++; /* skip comma */ |
|
|
|
|
/* block_sig hex */ |
|
|
|
|
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++; |
|
|
|
|
p += 128; if (n < nb - 1 && *p == ',') p++; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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, base_filename); |
|
|
|
|
|
|
|
|
|
char dest[1024]; snprintf(dest, sizeof(dest), "%s/%s", media_base, ch_id); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: auto-download ch=%s media=%02x%02x... blocks=%d size=%lld", |
|
|
|
|
CC_ID, ch_id, result.media_id[0], result.media_id[1], nb, (long long)fsize); |
|
|
|
|
media_download_start(inst, 0, &result, dest, media_base, NULL, NULL); |
|
|
|
|
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; |
|
|
|
|
if (author_sig) memcpy(ctx->author_sig, author_sig, 64); |
|
|
|
|
|
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: download start ch=%s media=%02x%02x... blocks=%d size=%lld dest=%s", |
|
|
|
|
CC_ID, ch_id, result.media_id[0], result.media_id[1], nb, (long long)fsize, dest); |
|
|
|
|
media_download_start(inst, 0, &result, dest, media_base, md_download_done_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) { |
|
|
|
|
if (!chat_setting_get_int("storage_autoload", 1)) return; |
|
|
|
|
int max_mb = chat_setting_get_int("storage_autoload_maxsize_mb", 10); |
|
|
|
|
|
|
|
|
|
const char* p = data_str; |
|
|
|
|
while (*p && *p != '|') p++; /* skip base filename */ |
|
|
|
|
if (!*p) return; p++; |
|
|
|
|
long long fsize = strtoll(p, (char**)&p, 10); |
|
|
|
|
if (fsize > (long long)max_mb * 1024 * 1024) { |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_DB_SYNC, "%s: media too large (%lld > %d MB), skip auto-download", |
|
|
|
|
CC_ID, fsize, max_mb); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
md_start_download(inst, data_str, data_len, ch_id, base_filename, ts, author_sig); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ─── 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; |
|
|
|
|
@ -481,20 +517,44 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char
|
|
|
|
|
|
|
|
|
|
/* auto-download media if message contains media metadata */ |
|
|
|
|
if (data && len > 0 && author != g_cc.my_node_id) { |
|
|
|
|
/* parse JSON: {"n":...,"ch":"...","ct":"...","d":"..."} */ |
|
|
|
|
const char* ct = strstr(data, "\"ct\":\""); |
|
|
|
|
const char* dpos = strstr(data, "\"d\":\""); |
|
|
|
|
if (ct && dpos) { |
|
|
|
|
const char* d_start = dpos + 5; |
|
|
|
|
/* find closing quote of "d" value */ |
|
|
|
|
char body[4096]; size_t bi = 0; |
|
|
|
|
while (*d_start && *d_start != '\"' && bi < sizeof(body) - 1) body[bi++] = *d_start++; |
|
|
|
|
body[bi] = '\0'; |
|
|
|
|
/* content_type is between ct\":\" and next \" */ |
|
|
|
|
const char* ct_val = ct + 6; |
|
|
|
|
/* determine media_base from chat_core context */ |
|
|
|
|
const char* media_base = "/tmp/utun_media"; |
|
|
|
|
md_auto_download(g_cc.inst, body, bi, ch_id, media_base); |
|
|
|
|
|
|
|
|
|
char base_filename[256]; size_t bfi = 0; |
|
|
|
|
while (body[bfi] && body[bfi] != '|' && bfi < sizeof(base_filename) - 1) |
|
|
|
|
base_filename[bfi++] = body[bfi]; |
|
|
|
|
base_filename[bfi] = '\0'; |
|
|
|
|
if (bfi == 0) snprintf(base_filename, sizeof(base_filename), "file"); |
|
|
|
|
|
|
|
|
|
const uint8_t* sig_field = strstr(data, "\"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 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); |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
sqlite3_finalize(st); |
|
|
|
|
} |
|
|
|
|
md_auto_download(g_cc.inst, body, bi, ch_id, base_filename, record_ts, author_sig); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -506,3 +566,63 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char
|
|
|
|
|
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 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 = (const uint8_t*)sqlite3_column_blob(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); |
|
|
|
|
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 base_filename[256]; size_t bfi = 0; |
|
|
|
|
while (body[bfi] && body[bfi] != '|' && bfi < sizeof(base_filename) - 1) |
|
|
|
|
base_filename[bfi++] = body[bfi]; |
|
|
|
|
base_filename[bfi] = '\0'; |
|
|
|
|
if (bfi == 0) snprintf(base_filename, sizeof(base_filename), "file"); |
|
|
|
|
|
|
|
|
|
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 file=%s", |
|
|
|
|
CC_ID, channel_id, (long long)msg_id, base_filename); |
|
|
|
|
md_start_download(g_cc.inst, body, bi, channel_id, base_filename, (uint64_t)db_ts, author_sig); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
|