diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index c493adff..60728ec0 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt @@ -97,6 +97,7 @@ object NativeLib { fun connectMemberNode(channelId: String, nodeId: Long) { nativeConnectMemberNode(channelId, nodeId) } fun getChannels(): String = nativeGetChannels() ?: "[]" fun getMessages(channelId: String, limit: Int = 50): String = nativeGetMessages(channelId, limit) ?: "[]" + fun feedbackCommand(json: String, requestId: Long = 0): Boolean = nativeFeedbackCommand(json.toByteArray(Charsets.UTF_8), requestId) fun clearDatabase(): Boolean = nativeClearDatabase() fun setDebugLevel(category: String, level: String) { nativeSetDebugLevel(category, level) } @@ -237,6 +238,7 @@ object NativeLib { private external fun nativeConnectMemberNode(channelId: String, nodeId: Long) private external fun nativeGetChannels(): String? private external fun nativeGetMessages(channelId: String, limit: Int): String? + private external fun nativeFeedbackCommand(json: ByteArray, requestId: Long): Boolean private external fun nativeSetDebugLevel(category: String, level: String) private external fun nativeSetChatSetting(name: String, value: String) private external fun nativeGetChatSettingInt(name: String, def: Int): Int diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index 274adf50..d426e342 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -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;