diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index 26749587..44cce9f6 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -184,16 +184,16 @@ int utun_bridge_start(const char* config_text) { void utun_bridge_poll(void) { } -void utun_bridge_send_message(const char* channel_id, const char* text) { - bridge_log(BLEV_DEBUG, "send ch=%s text=%s", channel_id ? channel_id : "", text ? text : ""); - if (!channel_id || !text) return; +int utun_bridge_send_message(const char* channel_id, const char* text, const uint8_t reply_to[32], uint64_t request_id) { + if (!channel_id || !text) { bridge_log(BLEV_ERROR, "send: invalid arguments"); return -1; } struct UASYNC* ua = instance_lite_get_uasync(); - if (!uasync_is_running(ua)) return; + if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "send: core stopped"); return -1; } size_t datalen = strlen(text); struct chat_msg_submit* req = u_calloc(1, sizeof(struct chat_msg_submit) + datalen); - if (!req) return; + struct posted_task* post = u_calloc(1, sizeof(*post)); + if (!req || !post) { bridge_log(BLEV_ERROR, "send: allocation failed"); u_free(req); u_free(post); return -1; } req->inst = instance_lite_get_instance(); strncpy(req->channel_id, channel_id, sizeof(req->channel_id) - 1); @@ -202,9 +202,13 @@ void utun_bridge_send_message(const char* channel_id, const char* text) { req->data_len = (uint32_t)datalen; memcpy(req->data, text, datalen); req->timestamp = 0; + memcpy(req->reply_to, reply_to, sizeof(req->reply_to)); + req->request_id = request_id; - uasync_post(ua, chat_core_submit_trampoline, req); - bridge_log(BLEV_DEBUG, "send queued ch=%s len=%zu", channel_id, datalen); + post->callback = chat_core_submit_trampoline; post->arg = req; + uasync_post_reserved(ua, post); + bridge_log(BLEV_INFO, "send queued ch=%s bytes=%zu request=%llu", channel_id, datalen, (unsigned long long)request_id); + return 0; } void utun_bridge_create_channel(const char* name, const char* channel_id) { @@ -500,21 +504,29 @@ char* utun_bridge_get_channels_json(void) { return json; } -char* utun_bridge_get_messages_json(const char* channel_id, int limit) { +char* utun_bridge_get_messages_json(const char* channel_id, int limit, const uint8_t* anchor, int exact) { 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("[]"); - char table[80], sql[1024]; msg_table_name(channel_id, table, sizeof(table)); + char table[80], sql[2048], selection[1024]; msg_table_name(channel_id, table, sizeof(table)); + if (exact && anchor) snprintf(selection, sizeof(selection), "SELECT * FROM \"%s\" WHERE message_id=?2", table); + else if (anchor) snprintf(selection, sizeof(selection), + "SELECT * FROM \"%s\" ORDER BY timestamp DESC,author_signature DESC LIMIT ?1 OFFSET min(" + "max(0,(SELECT count(*) FROM \"%s\" n JOIN \"%s\" a ON a.message_id=?2 " + "WHERE (n.timestamp,n.author_signature)>(a.timestamp,a.author_signature)) - ?1/2)," + "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," "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); + "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); 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); struct strbuf out = strbuf_new(); int step, result = strbuf_addf(&out, "["); unsigned count = 0; while (result >= 0 && (step = sqlite3_step(st)) == SQLITE_ROW) { @@ -566,6 +578,8 @@ char* utun_bridge_get_messages_json(const char* channel_id, int limit) { 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, ",\"replyTo\":"); + if (result >= 0) result = strbuf_add_json_string(&out, (const char*)sqlite3_column_text(st, 7)); if (result >= 0) result = strbuf_addf(&out, "}"); } sqlite3_finalize(st); @@ -1322,9 +1336,9 @@ int utun_bridge_is_responsive(void) { * Voice recording * ────────────────────────────────────────────────────────────────── */ -int utun_bridge_voice_start(const char* channel_id) { +int utun_bridge_voice_start(const char* channel_id, const uint8_t reply_to[32], uint64_t request_id) { voice_recorder_init(g_db_path); /* idempotent: гарантирует g_rec после stop/restart */ - return voice_recorder_start(channel_id, 48000, 1); + return voice_recorder_start(channel_id, 48000, 1, reply_to, request_id); } int utun_bridge_voice_feed(const int16_t* samples, int count) { @@ -1368,8 +1382,9 @@ float utun_bridge_voice_get_peak_level(void) { return voice_recorder_get_peak_level(); } -int utun_bridge_attachment_send(const char* channel_id, const char* file_path, const char* db_path) { - return attachment_send(channel_id, file_path, db_path); +int utun_bridge_attachment_send(const char* channel_id, const char* file_path, const char* db_path, + const uint8_t reply_to[32], uint64_t request_id) { + return attachment_send(channel_id, file_path, db_path, reply_to, request_id); } int utun_bridge_attachment_download(const char* channel_id, int64_t msg_id) { @@ -1387,13 +1402,13 @@ int utun_bridge_attachment_download(const char* channel_id, int64_t msg_id) { } int utun_bridge_video_send(const char* channel_id, const char* file_path, const char* db_path, - int duration_ms, int width, int height) { - return video_send(channel_id, file_path, db_path, duration_ms, width, height); + int duration_ms, int width, int height, const uint8_t reply_to[32], uint64_t request_id) { + return video_send(channel_id, file_path, db_path, duration_ms, width, height, reply_to, request_id); } int utun_bridge_photo_send(const char* channel_id, const char* file_path, const char* db_path, - int width, int height) { - return photo_send(channel_id, file_path, db_path, width, height); + int width, int height, const uint8_t reply_to[32], uint64_t request_id) { + return photo_send(channel_id, file_path, db_path, width, height, reply_to, request_id); } /* ────────────────────────────────────────────────────────────────── @@ -1896,14 +1911,31 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDestroy( if (g_event_callback) { (*env)->DeleteGlobalRef(env, g_event_callback); g_event_callback = NULL; } } -JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSendMessage( - JNIEnv* env, jobject thiz, jstring channelId, jstring text) { +/* Пустой массив означает отсутствие ссылки; иначе принимаем только полный глобальный ID. */ +static int jni_message_id(JNIEnv* env, jbyteArray value, uint8_t id[32]) { + memset(id, 0, 32); + int length = value ? (*env)->GetArrayLength(env, value) : -1; + if (length != 0 && length != 32) { bridge_log(BLEV_ERROR, "message ID: invalid byte length=%d", length); return -1; } + if (length) (*env)->GetByteArrayRegion(env, value, 0, 32, (jbyte*)id); + if ((*env)->ExceptionCheck(env)) { bridge_log(BLEV_ERROR, "message ID: JNI copy failed"); return -1; } + return 0; +} + +JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeSendMessage( + JNIEnv* env, jobject thiz, jstring channelId, jbyteArray text, jbyteArray replyTo, jlong requestId) { (void)thiz; + uint8_t reply[32]; + if (jni_message_id(env, replyTo, reply) < 0 || !text) return JNI_FALSE; + jsize length = (*env)->GetArrayLength(env, text); + if (length <= 0 || length > DB_SYNC_MESSAGE_MAX) { bridge_log(BLEV_ERROR, "send: invalid text bytes=%d", length); return JNI_FALSE; } + char tx[DB_SYNC_MESSAGE_MAX + 1]; + (*env)->GetByteArrayRegion(env, text, 0, length, (jbyte*)tx); tx[length] = 0; + if ((*env)->ExceptionCheck(env)) { bridge_log(BLEV_ERROR, "send: text copy failed"); return JNI_FALSE; } + if (memchr(tx, 0, (size_t)length)) { bridge_log(BLEV_ERROR, "send: embedded NUL in text"); return JNI_FALSE; } const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); - const char* tx = (*env)->GetStringUTFChars(env, text, NULL); - utun_bridge_send_message(ch, tx); - if (tx) (*env)->ReleaseStringUTFChars(env, text, tx); + int rc = utun_bridge_send_message(ch, tx, reply, (uint64_t)requestId); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); + return rc == 0 ? JNI_TRUE : JNI_FALSE; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCreateChannel( @@ -1936,10 +1968,12 @@ JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetChannels( } JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMessages( - JNIEnv* env, jobject thiz, jstring channelId, jint limit) { + JNIEnv* env, jobject thiz, jstring channelId, jint limit, jbyteArray anchor, jboolean exact) { (void)thiz; + uint8_t id[32]; + if (jni_message_id(env, anchor, id) < 0) return NULL; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); - char* json = utun_bridge_get_messages_json(ch, (int)limit); + char* json = utun_bridge_get_messages_json(ch, (int)limit, (*env)->GetArrayLength(env, anchor) ? id : NULL, exact); /* String(byte[], UTF-8) корректно восстанавливает четырёхбайтовые emoji. */ jclass strings = (*env)->FindClass(env, "java/lang/String"); jmethodID constructor = strings ? (*env)->GetMethodID(env, strings, "", "([BLjava/lang/String;)V") : NULL; @@ -2229,10 +2263,12 @@ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeClearDatabase /* ── Voice recording JNI ── */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceStart( - JNIEnv* env, jobject thiz, jstring channelId) { + JNIEnv* env, jobject thiz, jstring channelId, jbyteArray replyTo, jlong requestId) { (void)thiz; + uint8_t reply[32]; + if (jni_message_id(env, replyTo, reply) < 0) return JNI_FALSE; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); - int rc = utun_bridge_voice_start(ch); + int rc = utun_bridge_voice_start(ch, reply, (uint64_t)requestId); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); return (rc == 0) ? JNI_TRUE : JNI_FALSE; } @@ -2252,8 +2288,7 @@ JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceStop( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; int duration_ms = 0; - utun_bridge_voice_stop(&duration_ms); - return (jint)duration_ms; + return utun_bridge_voice_stop(&duration_ms) == 0 ? (jint)duration_ms : -1; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceCancel( @@ -2309,11 +2344,13 @@ JNIEXPORT jfloat JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceGetPeakLev /* ── Attachment JNI ── */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeAttachmentSend( - JNIEnv* env, jobject thiz, jstring channelId, jstring filePath) { + JNIEnv* env, jobject thiz, jstring channelId, jstring filePath, jbyteArray replyTo, jlong requestId) { (void)thiz; + uint8_t reply[32]; + if (jni_message_id(env, replyTo, reply) < 0) return JNI_FALSE; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); const char* fp = (*env)->GetStringUTFChars(env, filePath, NULL); - int rc = utun_bridge_attachment_send(ch, fp, g_db_path); + int rc = utun_bridge_attachment_send(ch, fp, g_db_path, reply, (uint64_t)requestId); if (fp) (*env)->ReleaseStringUTFChars(env, filePath, fp); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); return (rc == 0) ? JNI_TRUE : JNI_FALSE; @@ -2330,11 +2367,13 @@ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeAttachmentDow JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVideoSend( JNIEnv* env, jobject thiz, jstring channelId, jstring filePath, - jint durationMs, jint width, jint height) { + jint durationMs, jint width, jint height, jbyteArray replyTo, jlong requestId) { (void)thiz; + uint8_t reply[32]; + if (jni_message_id(env, replyTo, reply) < 0) return JNI_FALSE; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); const char* fp = (*env)->GetStringUTFChars(env, filePath, NULL); - int rc = utun_bridge_video_send(ch, fp, g_db_path, (int)durationMs, (int)width, (int)height); + int rc = utun_bridge_video_send(ch, fp, g_db_path, (int)durationMs, (int)width, (int)height, reply, (uint64_t)requestId); if (fp) (*env)->ReleaseStringUTFChars(env, filePath, fp); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); return (rc == 0) ? JNI_TRUE : JNI_FALSE; @@ -2342,11 +2381,13 @@ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVideoSend( JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativePhotoSend( JNIEnv* env, jobject thiz, jstring channelId, jstring filePath, - jint width, jint height) { + jint width, jint height, jbyteArray replyTo, jlong requestId) { (void)thiz; + uint8_t reply[32]; + if (jni_message_id(env, replyTo, reply) < 0) return JNI_FALSE; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); const char* fp = (*env)->GetStringUTFChars(env, filePath, NULL); - int rc = utun_bridge_photo_send(ch, fp, g_db_path, (int)width, (int)height); + int rc = utun_bridge_photo_send(ch, fp, g_db_path, (int)width, (int)height, reply, (uint64_t)requestId); if (fp) (*env)->ReleaseStringUTFChars(env, filePath, fp); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); return (rc == 0) ? JNI_TRUE : JNI_FALSE; diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.h b/tools/chatgui-android/jni_bridge/android_jni_bridge.h index 26a8af94..43c3d286 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.h +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.h @@ -47,7 +47,7 @@ void utun_bridge_destroy(void); /* ── Actions (Kotlin → C) ── */ -void utun_bridge_send_message(const char* channel_id, const char* text); +int utun_bridge_send_message(const char* channel_id, const char* text, const uint8_t reply_to[32], uint64_t request_id); void utun_bridge_create_channel(const char* name, const char* channel_id); void utun_bridge_connect_node(const char* address, int port, const char* pubkey_hex); @@ -68,7 +68,7 @@ void utun_bridge_connect_member_node(const char* channel_id, uint64_t node_id); /* ── Queries ── */ char* utun_bridge_get_channels_json(void); -char* utun_bridge_get_messages_json(const char* channel_id, int limit); +char* utun_bridge_get_messages_json(const char* channel_id, int limit, const uint8_t* anchor, int exact); uint64_t utun_bridge_get_my_node_id(void); /* ── Restart (config update, key regen, DB clear) ── */ @@ -86,7 +86,7 @@ int utun_bridge_is_responsive(void); /* ── Voice recording ── */ -int utun_bridge_voice_start(const char* channel_id); +int utun_bridge_voice_start(const char* channel_id, const uint8_t reply_to[32], uint64_t request_id); int utun_bridge_voice_feed(const int16_t* samples, int count); int utun_bridge_voice_stop(int* out_duration_ms); void utun_bridge_voice_cancel(void); @@ -101,16 +101,17 @@ float utun_bridge_voice_get_peak_level(void); /* ── Attachment ── */ -int utun_bridge_attachment_send(const char* channel_id, const char* file_path, const char* db_path); +int utun_bridge_attachment_send(const char* channel_id, const char* file_path, const char* db_path, + const uint8_t reply_to[32], uint64_t request_id); int utun_bridge_attachment_download(const char* channel_id, int64_t msg_id); /* Отправка видеосообщения (src — готовый mp4, без транскода) */ int utun_bridge_video_send(const char* channel_id, const char* file_path, const char* db_path, - int duration_ms, int width, int height); + int duration_ms, int width, int height, const uint8_t reply_to[32], uint64_t request_id); /* Отправка фото (src — готовый jpeg, без транскода) */ int utun_bridge_photo_send(const char* channel_id, const char* file_path, const char* db_path, - int width, int height); + int width, int height, const uint8_t reply_to[32], uint64_t request_id); /* ── Voice playback (Opus decode) ── */