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