|
|
|
|
@ -27,6 +27,7 @@
|
|
|
|
|
#include "../../../lib/mem.h" |
|
|
|
|
#include "../../../lib/sqlite3.h" |
|
|
|
|
#include "../../../src/chat/chat_core.h" |
|
|
|
|
#include "../../../src/media_async/encrypted_file.h" |
|
|
|
|
#include "../../../src/chat/chat_event.h" |
|
|
|
|
#include "../../../src/chat/chat_poll.h" |
|
|
|
|
#include "../../../lib/strbuf.h" |
|
|
|
|
@ -48,6 +49,7 @@
|
|
|
|
|
#include <stdarg.h> |
|
|
|
|
#include <limits.h> |
|
|
|
|
#include <unistd.h> |
|
|
|
|
#include <sys/stat.h> |
|
|
|
|
|
|
|
|
|
#ifdef __ANDROID__ |
|
|
|
|
#include <jni.h> |
|
|
|
|
@ -372,6 +374,7 @@ static void bridge_get_node_name(sqlite3* db, uint64_t node_id, const char* chan
|
|
|
|
|
|
|
|
|
|
/* Формирует человекочитаемое тело превью последнего сообщения по content_type и "d"-полю. */ |
|
|
|
|
static void bridge_format_preview_body(const char* ct, const char* d, char* out, size_t out_sz) { |
|
|
|
|
if (ct && !strcmp(ct, ENCRYPTED_ATTACHMENT_TYPE)) { out[0] = 0; return; } |
|
|
|
|
int is_text = (!ct || !ct[0] || strcmp(ct, "text/plain") == 0 || strcmp(ct, "voice_transcription") == 0); |
|
|
|
|
if (is_text) { |
|
|
|
|
size_t tl = strlen(d); |
|
|
|
|
@ -442,6 +445,7 @@ char* utun_bridge_get_channels_json(void) {
|
|
|
|
|
if (!ch_id) continue; |
|
|
|
|
|
|
|
|
|
char tbl_msg[80], tbl_peers[80], last_msg[160]; |
|
|
|
|
int last_encrypted = 0; |
|
|
|
|
msg_table_name(ch_id, tbl_msg, sizeof(tbl_msg)); |
|
|
|
|
peers_table_name(ch_id, tbl_peers, sizeof(tbl_peers)); |
|
|
|
|
last_msg[0] = '\0'; |
|
|
|
|
@ -460,6 +464,7 @@ char* utun_bridge_get_channels_json(void) {
|
|
|
|
|
char ct[32] = "text/plain"; |
|
|
|
|
const char* cts = strstr(dp, "\"ct\":\""); |
|
|
|
|
if (cts) { cts += 6; char* cte = strchr((char*)cts, '"'); if (cte) { size_t tlc = (size_t)(cte - cts); if (tlc < sizeof(ct)) { memcpy(ct, cts, tlc); ct[tlc] = '\0'; } } } |
|
|
|
|
last_encrypted = !strcmp(ct, ENCRYPTED_ATTACHMENT_TYPE); |
|
|
|
|
const char* ds = strstr(dp, "\"d\":\""); |
|
|
|
|
if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 511) tl = 511; char d[512]; memcpy(d, ds, tl); d[tl] = '\0'; bridge_format_preview_body(ct, d, body, sizeof(body)); } } |
|
|
|
|
} |
|
|
|
|
@ -491,12 +496,14 @@ char* utun_bridge_get_channels_json(void) {
|
|
|
|
|
const char* sep = first ? "" : ","; |
|
|
|
|
first = 0; |
|
|
|
|
|
|
|
|
|
size_t needed = snprintf(NULL, 0, "%s{\"id\":\"%s\",\"name\":\"%s\",\"lastMsgAt\":%lld,\"lastMsg\":\"%s\",\"peersOnline\":%d}", |
|
|
|
|
sep, ch_id, esc_name, (long long)last_msg_at, esc_msg, online); |
|
|
|
|
size_t needed = snprintf(NULL, 0, "%s{\"id\":\"%s\",\"name\":\"%s\",\"lastMsgAt\":%lld,\"lastMsg\":\"%s\"," |
|
|
|
|
"\"peersOnline\":%d,\"lastMsgEncrypted\":%s}", sep, ch_id, esc_name, (long long)last_msg_at, esc_msg, online, |
|
|
|
|
last_encrypted ? "true" : "false"); |
|
|
|
|
while (pos + needed + 2 > cap) { char* tmp = u_realloc(json, cap * 2); if (!tmp) break; json = tmp; cap *= 2; } |
|
|
|
|
if (pos + needed + 2 <= cap) |
|
|
|
|
pos += (size_t)snprintf(json + pos, cap - pos, "%s{\"id\":\"%s\",\"name\":\"%s\",\"lastMsgAt\":%lld,\"lastMsg\":\"%s\",\"peersOnline\":%d}", |
|
|
|
|
sep, ch_id, esc_name, (long long)last_msg_at, esc_msg, online); |
|
|
|
|
pos += (size_t)snprintf(json + pos, cap - pos, "%s{\"id\":\"%s\",\"name\":\"%s\",\"lastMsgAt\":%lld,\"lastMsg\":\"%s\"," |
|
|
|
|
"\"peersOnline\":%d,\"lastMsgEncrypted\":%s}", sep, ch_id, esc_name, (long long)last_msg_at, esc_msg, online, |
|
|
|
|
last_encrypted ? "true" : "false"); |
|
|
|
|
u_free(esc_msg); |
|
|
|
|
u_free(esc_name); |
|
|
|
|
} |
|
|
|
|
@ -518,17 +525,21 @@ char* utun_bridge_get_messages_json(const char* channel_id, int limit, const uin
|
|
|
|
|
"max(0,(SELECT count(*) FROM \"%s\")-?1))", table, table, table, table); |
|
|
|
|
else snprintf(selection, sizeof(selection), "SELECT * FROM \"%s\" ORDER BY timestamp DESC,author_signature DESC LIMIT ?1", table); |
|
|
|
|
snprintf(sql, sizeof(sql), |
|
|
|
|
"SELECT id,node_id,timestamp,local_attrs,message_id," |
|
|
|
|
"SELECT m.id,m.node_id,m.timestamp,m.local_attrs,m.message_id," |
|
|
|
|
"CASE WHEN json_valid(CAST(data AS TEXT)) THEN json_extract(CAST(data AS TEXT),'$.ct') END," |
|
|
|
|
"CASE WHEN json_valid(CAST(data AS TEXT)) THEN json_extract(CAST(data AS TEXT),'$.d') END," |
|
|
|
|
"CASE WHEN json_valid(CAST(data AS TEXT)) THEN json_extract(CAST(data AS TEXT),'$.reply_to') END" |
|
|
|
|
" FROM (%s) ORDER BY timestamp,author_signature", selection); |
|
|
|
|
"CASE WHEN json_valid(CAST(data AS TEXT)) THEN json_extract(CAST(data AS TEXT),'$.reply_to') END," |
|
|
|
|
"a.ready,a.downloaded,a.decrypting,a.error,a.plain_path,a.info,a.content_type,COALESCE(r.state,-1),length(a.key)=32" |
|
|
|
|
" FROM (%s) m LEFT JOIN attachment_keys a ON a.channel=?3 AND a.message_id=m.message_id" |
|
|
|
|
" LEFT JOIN attachment_key_requests r ON r.channel=a.channel AND r.message_id=a.message_id AND r.incoming=0" |
|
|
|
|
" ORDER BY m.timestamp,m.author_signature", selection); |
|
|
|
|
sqlite3_stmt* st = NULL; |
|
|
|
|
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: messages query ch=%s: %s", channel_id, sqlite3_errmsg(db)); return u_strdup("[]"); |
|
|
|
|
} |
|
|
|
|
sqlite3_bind_int(st, 1, limit > 0 ? limit : 50); |
|
|
|
|
if (anchor) sqlite3_bind_blob(st, 2, anchor, 32, SQLITE_STATIC); |
|
|
|
|
sqlite3_bind_text(st, 3, channel_id, -1, SQLITE_STATIC); |
|
|
|
|
struct strbuf out = strbuf_new(); int step, result = strbuf_addf(&out, "["); |
|
|
|
|
unsigned count = 0; |
|
|
|
|
while (result >= 0 && (step = sqlite3_step(st)) == SQLITE_ROW) { |
|
|
|
|
@ -542,8 +553,9 @@ char* utun_bridge_get_messages_json(const char* channel_id, int limit, const uin
|
|
|
|
|
char message_id[65], text[DB_SYNC_MESSAGE_MAX + 1]; |
|
|
|
|
for (unsigned i = 0; i < 32; i++) snprintf(message_id + 2 * i, 3, "%02x", id[i]); |
|
|
|
|
snprintf(text, sizeof(text), "%s", body ? body : ""); if (!type) type = "text/plain"; |
|
|
|
|
int encrypted = !strcmp(type, ENCRYPTED_ATTACHMENT_TYPE); |
|
|
|
|
/* У вложений '*' кодирует отображаемое имя; poll использует отдельный b64: формат. */ |
|
|
|
|
if (strcmp(type, "text/plain") && text[0] == '*') { |
|
|
|
|
if (!encrypted && strcmp(type, "text/plain") && text[0] == '*') { |
|
|
|
|
char* pipe = strchr(text, '|'); |
|
|
|
|
if (pipe) { |
|
|
|
|
uint8_t decoded[1024]; size_t bytes = b64_decode(text + 1, pipe - text - 1, decoded, sizeof(decoded)); |
|
|
|
|
@ -554,7 +566,7 @@ char* utun_bridge_get_messages_json(const char* channel_id, int limit, const uin
|
|
|
|
|
char author[128], file_path[1024] = ""; |
|
|
|
|
bridge_get_node_name(db, (uint64_t)node_id, channel_id, author, sizeof(author)); |
|
|
|
|
if (!author[0]) snprintf(author, sizeof(author), "0x%016llx", (unsigned long long)node_id); |
|
|
|
|
if (attrs) { |
|
|
|
|
if (!encrypted && attrs) { |
|
|
|
|
sqlite3_stmt* file = NULL; |
|
|
|
|
if (sqlite3_prepare_v2(db, "SELECT CASE WHEN json_valid(?1) THEN json_extract(?1,'$.fp') END", -1, &file, NULL) == SQLITE_OK) { |
|
|
|
|
sqlite3_bind_text(file, 1, attrs, -1, SQLITE_STATIC); |
|
|
|
|
@ -568,6 +580,46 @@ char* utun_bridge_get_messages_json(const char* channel_id, int limit, const uin
|
|
|
|
|
sqlite3_finalize(file); |
|
|
|
|
} else DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: local attributes query: %s", sqlite3_errmsg(db)); |
|
|
|
|
} |
|
|
|
|
const char* encryption_state = ""; |
|
|
|
|
struct attachment_info info = {0}; |
|
|
|
|
uint64_t file_size = 0; |
|
|
|
|
if (encrypted) { |
|
|
|
|
/* Шифротекст никогда не передаём UI как готовый файл; метаданные доступны только после decrypt. */ |
|
|
|
|
const char* pipe = body ? strchr(body, '|') : NULL; |
|
|
|
|
if (pipe) file_size = strtoull(pipe + 1, NULL, 10); |
|
|
|
|
text[0] = 0; |
|
|
|
|
const char* path = (const char*)sqlite3_column_text(st, 12); |
|
|
|
|
const char* plain_type = (const char*)sqlite3_column_text(st, 14); |
|
|
|
|
const int ready = sqlite3_column_int(st, 8), downloaded = sqlite3_column_int(st, 9); |
|
|
|
|
const int decrypting = sqlite3_column_int(st, 10), error = sqlite3_column_int(st, 11); |
|
|
|
|
const int state = sqlite3_column_int(st, 15), key = sqlite3_column_int(st, 16); |
|
|
|
|
struct stat file; |
|
|
|
|
int available = ready && path && !stat(path, &file) && S_ISREG(file.st_mode); |
|
|
|
|
if (available && (!plain_type || !plain_type[0] || attachment_decode(sqlite3_column_blob(st, 13), |
|
|
|
|
sqlite3_column_bytes(st, 13), &info))) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "bridge: encrypted metadata invalid ch=%s id=%lld", channel_id, (long long)local_id); |
|
|
|
|
available = 0; encryption_state = "error"; |
|
|
|
|
} |
|
|
|
|
if (available) { |
|
|
|
|
int n = snprintf(file_path, sizeof(file_path), "%s", path); |
|
|
|
|
if (n >= (int)sizeof(file_path)) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "bridge: decrypted path too long ch=%s id=%lld", channel_id, (long long)local_id); |
|
|
|
|
file_path[0] = 0; encryption_state = "error"; |
|
|
|
|
} else { |
|
|
|
|
encryption_state = "ready"; type = plain_type; file_size = (uint64_t)file.st_size; |
|
|
|
|
snprintf(text, sizeof(text), "%s", info.name); |
|
|
|
|
} |
|
|
|
|
} else if (!encryption_state[0]) { |
|
|
|
|
if (error) encryption_state = "error"; |
|
|
|
|
else if (state == 3) encryption_state = "denied"; |
|
|
|
|
else if (decrypting) encryption_state = "decrypting"; |
|
|
|
|
else if (ready) encryption_state = "download"; /* Открытая копия могла быть удалена кешем. */ |
|
|
|
|
else if (downloaded && key) encryption_state = "decrypting"; |
|
|
|
|
else if (state == 1) encryption_state = "pending"; |
|
|
|
|
else if (state == 0 || downloaded) encryption_state = "requesting"; |
|
|
|
|
else encryption_state = "download"; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
result = strbuf_addf(&out, "%s{\"id\":%lld,\"messageId\":\"%s\",\"authorNodeId\":%lld,\"author\":", |
|
|
|
|
count++ ? "," : "", (long long)local_id, message_id, (long long)node_id); |
|
|
|
|
if (result >= 0) result = strbuf_add_json_string(&out, author); |
|
|
|
|
@ -582,6 +634,12 @@ char* utun_bridge_get_messages_json(const char* channel_id, int limit, const uin
|
|
|
|
|
if (result >= 0) result = strbuf_add_json_string(&out, attrs); |
|
|
|
|
if (result >= 0) result = strbuf_addf(&out, ",\"replyTo\":"); |
|
|
|
|
if (result >= 0) result = strbuf_add_json_string(&out, (const char*)sqlite3_column_text(st, 7)); |
|
|
|
|
if (result >= 0 && encrypted) { |
|
|
|
|
result = strbuf_addf(&out, ",\"encryptionState\":\"%s\",\"fileSize\":%llu,\"durationMs\":%u," |
|
|
|
|
"\"width\":%u,\"height\":%u,\"waveform\":", encryption_state, (unsigned long long)file_size, |
|
|
|
|
info.duration_ms, info.width, info.height); |
|
|
|
|
if (result >= 0) result = strbuf_add_json_string(&out, info.waveform); |
|
|
|
|
} |
|
|
|
|
if (result >= 0) result = strbuf_addf(&out, "}"); |
|
|
|
|
} |
|
|
|
|
sqlite3_finalize(st); |
|
|
|
|
|