Browse Source

android: expose reply submission and anchored history in bridge

master
evgeny 3 days ago
parent
commit
368f629948
  1. 115
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  2. 13
      tools/chatgui-android/jni_bridge/android_jni_bridge.h

115
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, "<init>", "([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;

13
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) ── */

Loading…
Cancel
Save