|
|
|
|
@ -27,6 +27,8 @@
|
|
|
|
|
#include "../../../lib/mem.h" |
|
|
|
|
#include "../../../lib/sqlite3.h" |
|
|
|
|
#include "../../../src/chat/chat_core.h" |
|
|
|
|
#include "../../../src/chat/chat_poll.h" |
|
|
|
|
#include "../../../lib/strbuf.h" |
|
|
|
|
#include "../../../src/chat/chat_member.h" |
|
|
|
|
#include "../../../src/chat/group_avatar.h" |
|
|
|
|
#include "../../../src/member_files/member_files.h" |
|
|
|
|
@ -499,104 +501,79 @@ char* utun_bridge_get_channels_json(void) {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
char* utun_bridge_get_messages_json(const char* channel_id, int limit) { |
|
|
|
|
struct UTUN_INSTANCE* inst = instance_lite_get_instance(); |
|
|
|
|
sqlite3* db = chat_core_get_db(inst); |
|
|
|
|
struct UTUN_INSTANCE* inst = instance_lite_get_instance(); sqlite3* db = chat_core_get_db(inst); |
|
|
|
|
if (!chat_core_is_initialized(inst) || !db || !channel_id || !channel_id[0]) return u_strdup("[]"); |
|
|
|
|
uint64_t my_id = inst->node_id; |
|
|
|
|
|
|
|
|
|
char tbl_msg[80]; |
|
|
|
|
msg_table_name(channel_id, tbl_msg, sizeof(tbl_msg)); |
|
|
|
|
|
|
|
|
|
char sql[512]; |
|
|
|
|
char table[80], sql[1024]; msg_table_name(channel_id, table, sizeof(table)); |
|
|
|
|
snprintf(sql, sizeof(sql), |
|
|
|
|
"SELECT id, node_id, data, timestamp, local_attrs FROM (" |
|
|
|
|
"SELECT id, node_id, data, timestamp, local_attrs, author_signature FROM \"%s\"" |
|
|
|
|
" ORDER BY timestamp DESC, author_signature DESC LIMIT %d" |
|
|
|
|
") ORDER BY timestamp ASC, author_signature ASC", tbl_msg, limit); |
|
|
|
|
|
|
|
|
|
"SELECT id,node_id,timestamp,local_attrs,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" |
|
|
|
|
" FROM (SELECT * FROM \"%s\" ORDER BY timestamp DESC,author_signature DESC LIMIT ?)" |
|
|
|
|
" ORDER BY timestamp,author_signature", table); |
|
|
|
|
sqlite3_stmt* st = NULL; |
|
|
|
|
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return u_strdup("[]"); |
|
|
|
|
|
|
|
|
|
size_t cap = 4096, pos = 1; |
|
|
|
|
char* json = u_malloc(cap); |
|
|
|
|
if (!json) { sqlite3_finalize(st); return u_strdup("[]"); } |
|
|
|
|
json[0] = '['; |
|
|
|
|
|
|
|
|
|
int first = 1; |
|
|
|
|
while (sqlite3_step(st) == SQLITE_ROW) { |
|
|
|
|
int64_t msg_id = sqlite3_column_int64(st, 0); |
|
|
|
|
int64_t node_id = sqlite3_column_int64(st, 1); |
|
|
|
|
int64_t ts = sqlite3_column_int64(st, 3); |
|
|
|
|
const uint8_t* dptr = sqlite3_column_blob(st, 2); |
|
|
|
|
int dlen = sqlite3_column_bytes(st, 2); |
|
|
|
|
const char* lattr = (const char*)sqlite3_column_text(st, 4); |
|
|
|
|
|
|
|
|
|
char txt[2048] = "", ct[64] = "text/plain"; |
|
|
|
|
if (dptr && dlen > 0) { |
|
|
|
|
const char* ds = strstr((const char*)dptr, "\"d\":\""); |
|
|
|
|
if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 2000) tl = 2000; memcpy(txt, ds, tl); txt[tl] = '\0'; } } |
|
|
|
|
const char* cts = strstr((const char*)dptr, "\"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'; } } } |
|
|
|
|
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); |
|
|
|
|
struct strbuf out = strbuf_new(); int step, result = strbuf_addf(&out, "["); |
|
|
|
|
unsigned count = 0; |
|
|
|
|
while (result >= 0 && (step = sqlite3_step(st)) == SQLITE_ROW) { |
|
|
|
|
int64_t local_id = sqlite3_column_int64(st, 0), node_id = sqlite3_column_int64(st, 1), ts = sqlite3_column_int64(st, 2); |
|
|
|
|
const char* attrs = (const char*)sqlite3_column_text(st, 3); |
|
|
|
|
const uint8_t* id = sqlite3_column_blob(st, 4); |
|
|
|
|
const char* type = (const char*)sqlite3_column_text(st, 5), *body = (const char*)sqlite3_column_text(st, 6); |
|
|
|
|
if (sqlite3_column_bytes(st, 4) != 32 || sqlite3_column_bytes(st, 6) > DB_SYNC_MESSAGE_MAX) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: corrupt message ch=%s id=%lld", channel_id, (long long)local_id); result = -1; break; |
|
|
|
|
} |
|
|
|
|
/* base64-decode display prefix for media messages if marked with '*' (new format) */ |
|
|
|
|
if (strcmp(ct, "text/plain") != 0 && txt[0] == '*') { |
|
|
|
|
char* pipe = strchr(txt, '|'); |
|
|
|
|
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"; |
|
|
|
|
/* У вложений '*' кодирует отображаемое имя; poll использует отдельный b64: формат. */ |
|
|
|
|
if (strcmp(type, "text/plain") && text[0] == '*') { |
|
|
|
|
char* pipe = strchr(text, '|'); |
|
|
|
|
if (pipe) { |
|
|
|
|
size_t prefix_len = (size_t)(pipe - txt); /* includes '*' */ |
|
|
|
|
uint8_t dec[1024]; int declen = (int)b64_decode(txt + 1, prefix_len - 1, dec, sizeof(dec)); |
|
|
|
|
if (declen > 0 && declen < 2000) { |
|
|
|
|
/* shift suffix left, place decoded at start */ |
|
|
|
|
size_t suffix_len = strlen(pipe); /* includes '|' */ |
|
|
|
|
memmove(txt + declen, pipe, suffix_len + 1); |
|
|
|
|
memcpy(txt, dec, (size_t)declen); |
|
|
|
|
} |
|
|
|
|
uint8_t decoded[1024]; size_t bytes = b64_decode(text + 1, pipe - text - 1, decoded, sizeof(decoded)); |
|
|
|
|
size_t suffix = strlen(pipe); |
|
|
|
|
if (bytes && bytes + suffix < sizeof(text)) { memmove(text + bytes, pipe, suffix + 1); memcpy(text, decoded, bytes); } |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
char* esc_txt = json_escape_alloc(txt); |
|
|
|
|
char* esc_ct = json_escape_alloc(ct); |
|
|
|
|
int is_out = (node_id == (int64_t)my_id) ? 1 : 0; |
|
|
|
|
const char* sep = first ? "" : ","; |
|
|
|
|
first = 0; |
|
|
|
|
|
|
|
|
|
char author_name[128]; |
|
|
|
|
bridge_get_node_name(db, (uint64_t)node_id, channel_id, author_name, sizeof(author_name)); |
|
|
|
|
if (!author_name[0]) |
|
|
|
|
snprintf(author_name, sizeof(author_name), "0x%016llx", (unsigned long long)node_id); |
|
|
|
|
|
|
|
|
|
/* build filePath for voice/attachment messages */ |
|
|
|
|
char file_path[1024] = ""; |
|
|
|
|
if (lattr) { |
|
|
|
|
const char* fps = strstr(lattr, "\"fp\":\""); |
|
|
|
|
if (fps) { fps += 6; |
|
|
|
|
char* fpe = strchr((char*)fps, '"'); |
|
|
|
|
if (fpe) { size_t fpl = (size_t)(fpe - fps); |
|
|
|
|
snprintf(file_path, sizeof(file_path), "%s/media/%s/%.*s", |
|
|
|
|
g_db_path, channel_id, (int)fpl, fps); |
|
|
|
|
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) { |
|
|
|
|
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); |
|
|
|
|
if (sqlite3_step(file) == SQLITE_ROW && sqlite3_column_type(file, 0) == SQLITE_TEXT) { |
|
|
|
|
int bytes = snprintf(file_path, sizeof(file_path), "%s/media/%s/%s", |
|
|
|
|
g_db_path, channel_id, sqlite3_column_text(file, 0)); |
|
|
|
|
if (bytes >= (int)sizeof(file_path)) { |
|
|
|
|
file_path[0] = 0; DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "bridge: media path too long ch=%s", channel_id); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
sqlite3_finalize(file); |
|
|
|
|
} else DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: local attributes query: %s", sqlite3_errmsg(db)); |
|
|
|
|
} |
|
|
|
|
char* esc_fp = file_path[0] ? json_escape_alloc(file_path) : NULL; |
|
|
|
|
char* esc_la = lattr && lattr[0] ? json_escape_alloc(lattr) : NULL; |
|
|
|
|
|
|
|
|
|
size_t needed = snprintf(NULL, 0, "%s{\"id\":%lld,\"author\":\"%s\",\"authorNodeId\":%lld,\"text\":\"%s\",\"ts\":%lld,\"isOutgoing\":%s,\"contentType\":\"%s\"%s%s%s%s%s%s}", |
|
|
|
|
sep, (long long)msg_id, author_name, (long long)node_id, esc_txt, (long long)ts, is_out ? "true" : "false", esc_ct, |
|
|
|
|
esc_fp ? ",\"filePath\":\"" : "", esc_fp ? esc_fp : "", esc_fp ? "\"" : "", |
|
|
|
|
esc_la ? ",\"localAttrs\":\"" : "", esc_la ? esc_la : "", esc_la ? "\"" : ""); |
|
|
|
|
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\":%lld,\"author\":\"%s\",\"authorNodeId\":%lld,\"text\":\"%s\",\"ts\":%lld,\"isOutgoing\":%s,\"contentType\":\"%s\"%s%s%s%s%s%s}", |
|
|
|
|
sep, (long long)msg_id, author_name, (long long)node_id, esc_txt, (long long)ts, is_out ? "true" : "false", esc_ct, |
|
|
|
|
esc_fp ? ",\"filePath\":\"" : "", esc_fp ? esc_fp : "", esc_fp ? "\"" : "", |
|
|
|
|
esc_la ? ",\"localAttrs\":\"" : "", esc_la ? esc_la : "", esc_la ? "\"" : ""); |
|
|
|
|
if (esc_la) u_free(esc_la); |
|
|
|
|
if (esc_fp) u_free(esc_fp); |
|
|
|
|
u_free(esc_ct); |
|
|
|
|
u_free(esc_txt); |
|
|
|
|
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); |
|
|
|
|
if (result >= 0) result = strbuf_addf(&out, ",\"ts\":%lld,\"isOutgoing\":%s,\"text\":", |
|
|
|
|
(long long)ts, node_id == (int64_t)inst->node_id ? "true" : "false"); |
|
|
|
|
if (result >= 0) result = strbuf_add_json_string(&out, text); |
|
|
|
|
if (result >= 0) result = strbuf_addf(&out, ",\"contentType\":"); |
|
|
|
|
if (result >= 0) result = strbuf_add_json_string(&out, type); |
|
|
|
|
if (result >= 0) result = strbuf_addf(&out, ",\"filePath\":"); |
|
|
|
|
if (result >= 0) result = strbuf_add_json_string(&out, file_path); |
|
|
|
|
if (result >= 0) result = strbuf_addf(&out, ",\"localAttrs\":"); |
|
|
|
|
if (result >= 0) result = strbuf_add_json_string(&out, attrs); |
|
|
|
|
if (result >= 0) result = strbuf_addf(&out, "}"); |
|
|
|
|
} |
|
|
|
|
sqlite3_finalize(st); |
|
|
|
|
|
|
|
|
|
json[pos++] = ']'; json[pos] = '\0'; |
|
|
|
|
return json; |
|
|
|
|
if (result < 0 || step != SQLITE_DONE || strbuf_addf(&out, "]") < 0) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: messages snapshot failed ch=%s: %s", channel_id, sqlite3_errmsg(db)); |
|
|
|
|
strbuf_free(&out); return u_strdup("[]"); |
|
|
|
|
} |
|
|
|
|
return strbuf_detach(&out); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
uint64_t utun_bridge_get_my_node_id(void) { |
|
|
|
|
@ -1952,6 +1929,27 @@ JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMessages(
|
|
|
|
|
return result; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* UTF-8 JSON приходит байтами, чтобы emoji не превращались в JNI modified UTF-8. */ |
|
|
|
|
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeFeedbackCommand( |
|
|
|
|
JNIEnv* env, jobject thiz, jbyteArray json, jlong request_id) { |
|
|
|
|
(void)thiz; |
|
|
|
|
struct UASYNC* ua = instance_lite_get_uasync(); |
|
|
|
|
struct UTUN_INSTANCE* inst = instance_lite_get_instance(); |
|
|
|
|
if (!json || !inst || !uasync_is_running(ua)) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: feedback core unavailable"); return JNI_FALSE; |
|
|
|
|
} |
|
|
|
|
jsize len = (*env)->GetArrayLength(env, json); |
|
|
|
|
if (len <= 0 || len > 16384) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: feedback command size=%d", len); return JNI_FALSE; |
|
|
|
|
} |
|
|
|
|
struct chat_feedback_request* req = u_malloc(sizeof(*req) + len + 1); |
|
|
|
|
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: feedback allocation failed"); return JNI_FALSE; } |
|
|
|
|
req->inst = inst; req->request_id = (uint64_t)request_id; |
|
|
|
|
(*env)->GetByteArrayRegion(env, json, 0, len, (jbyte*)(req + 1)); |
|
|
|
|
if ((*env)->ExceptionCheck(env)) { u_free(req); DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: feedback JNI copy failed"); return JNI_FALSE; } |
|
|
|
|
((char*)(req + 1))[len] = 0; uasync_post(ua, chat_feedback_command_trampoline, req); return JNI_TRUE; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDmStart( |
|
|
|
|
JNIEnv* env, jobject thiz, jlong nodeId, jstring sourceChId, jstring peerName) { |
|
|
|
|
(void)thiz; |
|
|
|
|
|