Browse Source

Expose UTF-8 feedback commands and full message identities on Android

master
evgeny 5 days ago
parent
commit
2c31e3fe03
  1. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt
  2. 172
      tools/chatgui-android/jni_bridge/android_jni_bridge.c

2
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

172
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;

Loading…
Cancel
Save