/* * android_jni_bridge.c — JNI bridge implementation * * C side of the bridge. Wires Kotlin ↔ uTun stack: * - nativeStart(configText) → instance_lite_start() * - nativeJoinChannel(chId, nodeId, pubkey, addrs) → chat_sync_connect_from_invite() * - Chat events → JNI callback → Kotlin */ #include "android_jni_bridge.h" #include "android_udp_log.h" #include "../libutun_lite/utun_config_api.h" #include "../libutun_lite/invite_link_c.h" #include "../libutun_lite/instance_lite.h" #include "../libutun_lite/voice_recorder.h" #include "../libutun_lite/attachment_sender.h" #include "../libutun_lite/video_sender.h" #include "../libutun_lite/photo_sender.h" #include "../libutun_lite/standby.h" #include "../../../src/call/call_audio.h" #include "../../../src/call/call.h" #include "../../../src/radio/radio.h" #include "../../../src/radio/radio_audio.h" #include "../../../src/dm/dm_core.h" #include "../../../src/dm/dm_media.h" #include "../../../lib/debug_config.h" #include "../../../lib/u_async.h" #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" #include "../../../src/chat/invite_build.h" #include "../../../src/chat/chat_setting.h" #include "../../../src/routing_layer/topo_node_sqlite.h" #include "../../../src/utun_instance.h" #include "../../../src/transport_layer/etcp.h" #include "../../../src/transport_layer/etcp_connections.h" #include "../../../src/transport_layer/auto_socket.h" #include "../../../src/transport_layer/secure_channel.h" #include #include #include #include #include #include #include #ifdef __ANDROID__ #include #include #define LOG_TAG "utun-bridge" #else #define ANDROID_LOG_VERBOSE 2 #define ANDROID_LOG_DEBUG 3 #define ANDROID_LOG_INFO 4 #define ANDROID_LOG_WARN 5 #define ANDROID_LOG_ERROR 6 #endif enum { BLEV_TRACE = 0, BLEV_DEBUG = 1, BLEV_INFO = 2, BLEV_WARN = 3, BLEV_ERROR = 4 }; static int blevel_to_android(int blevel) { switch (blevel) { case BLEV_TRACE: return ANDROID_LOG_VERBOSE; case BLEV_DEBUG: return ANDROID_LOG_DEBUG; case BLEV_INFO: return ANDROID_LOG_INFO; case BLEV_WARN: return ANDROID_LOG_WARN; case BLEV_ERROR: return ANDROID_LOG_ERROR; default: return ANDROID_LOG_VERBOSE; } } static utun_log_fn g_on_log = NULL; static utun_event_fn g_on_event = NULL; static char g_db_path[512]; static void bridge_log(int level, const char* fmt, ...) { va_list ap; char fmt_buf[1024]; va_start(ap, fmt); vsnprintf(fmt_buf, sizeof(fmt_buf), fmt, ap); va_end(ap); #ifdef __ANDROID__ __android_log_print(blevel_to_android(level), LOG_TAG, "%s", fmt_buf); #else fprintf(stderr, "%s\n", fmt_buf); #endif if (g_on_log) g_on_log(level, "JNI", fmt_buf); } static void bridge_debug_hook(int level, const char* category, const char* message) { /* DEBUG_*: ERROR=1..TRACE=5; bridge/Kotlin: TRACE=0..ERROR=4. */ int bridge_level = level >= DEBUG_LEVEL_ERROR && level <= DEBUG_LEVEL_TRACE ? DEBUG_LEVEL_TRACE - level : BLEV_DEBUG; #ifdef __ANDROID__ __android_log_print(blevel_to_android(bridge_level), LOG_TAG, "[%s] %s", category ? category : "?", message ? message : ""); #else fprintf(stderr, "[%s] %s\n", category ? category : "?", message ? message : ""); #endif udp_log_write(message, (int)strlen(message)); if (level == DEBUG_LEVEL_ERROR) udp_log_flush(); if (g_on_log) g_on_log(bridge_level, category, message); } static utun_config_string_fn g_cfg_string = NULL; static utun_config_int64_fn g_cfg_int64 = NULL; static utun_config_int_fn g_cfg_int = NULL; static const char* cfg_wrapper_string(const char* key, const char* def) { if (g_cfg_string) { const char* val = g_cfg_string(key); return val ? val : def; } return def; } static int64_t cfg_wrapper_int64(const char* key, int64_t def) { if (g_cfg_int64) return g_cfg_int64(key); return def; } static int cfg_wrapper_int(const char* key, int def) { if (g_cfg_int) return g_cfg_int(key); return def; } /* ── Public API ── */ void utun_bridge_log(int level, const char* category, const char* message) { if (g_on_log) g_on_log(level, category, message); } void utun_bridge_event(int event_type, const char* json_data) { if (g_on_event) g_on_event(event_type, json_data); } bool utun_bridge_init(const char* db_path, int control_port, utun_log_fn on_log, utun_event_fn on_event, const struct utun_bridge_config* config) { g_on_log = on_log; g_on_event = on_event; debug_set_log_hook(bridge_debug_hook); if (config) { g_cfg_string = config->get_string; g_cfg_int64 = config->get_int64; g_cfg_int = config->get_int; utun_config_provider_t provider = { .get_string = cfg_wrapper_string, .get_int64 = cfg_wrapper_int64, .get_int = cfg_wrapper_int, .get_bool = NULL, }; utun_config_set_provider(&provider); } bridge_log(BLEV_INFO, "bridge init: db=%s port=%d (stub, use nativeStart for real init)", db_path ? db_path : "(null)", control_port); voice_recorder_init(db_path); return true; } void utun_bridge_destroy(void) { voice_recorder_deinit(); instance_lite_stop(); g_on_log = NULL; g_cfg_string = NULL; g_cfg_int64 = NULL; g_cfg_int = NULL; bridge_log(BLEV_INFO, "bridge destroy"); } int utun_bridge_start(const char* config_text) { bridge_log(BLEV_INFO, "bridge start: launching utun instance"); int rc = instance_lite_start(config_text); if (rc != 0) { bridge_log(BLEV_ERROR, "bridge start: instance_lite_start failed rc=%d", rc); } else { bridge_log(BLEV_INFO, "bridge start: instance thread created"); } return rc; } void utun_bridge_poll(void) { } 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)) { 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); 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); strncpy(req->content_type, "text/plain", sizeof(req->content_type) - 1); req->data = (uint8_t*)(req + 1); 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; 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) { bridge_log(BLEV_INFO, "create_channel name=%s id=%s", name ? name : "", channel_id ? channel_id : ""); if (!name || !name[0]) { bridge_log(BLEV_ERROR, "create_channel: empty name"); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "create_channel: uasync not running"); return; } struct chat_create_auto_arg* req = u_calloc(1, sizeof(*req)); if (!req) { bridge_log(BLEV_ERROR, "create_channel: OOM"); return; } req->inst = instance_lite_get_instance(); snprintf(req->name, sizeof(req->name), "%s", name); uasync_post(ua, chat_core_create_channel_auto_trampoline, req); } void utun_bridge_connect_node(const char* address, int port, const char* pubkey_hex) { bridge_log(BLEV_INFO, "connect %s:%d pubkey=%s", address ? address : "", port, pubkey_hex ? pubkey_hex : ""); } /* Real implementation: calls chat_sync_connect_from_invite() */ #include "chat/chat_sync.h" #include "chat/chat_core.h" #include "chat/chat_core_priv.h" void utun_bridge_join_channel(uint64_t channel_id, uint64_t node_id, const uint8_t* pubkey_bin, const uint8_t* addrs_data, int addr_count, int addrs_data_len, uint64_t join_key) { uint64_t real_node_id = sc_derive_node_id_from_pubkey(pubkey_bin); if (real_node_id != node_id) bridge_log(BLEV_WARN, "join: node_id Kotlin=0x%016llx != C=0x%016llx — using C value", (unsigned long long)node_id, (unsigned long long)real_node_id); bridge_log(BLEV_INFO, "join ch=%llu node=0x%016llx addrs=%d len=%d", (unsigned long long)channel_id, (unsigned long long)real_node_id, addr_count, addrs_data_len); if (!pubkey_bin || !addrs_data || addr_count <= 0) { bridge_log(BLEV_ERROR, "join invalid args"); utun_bridge_event(6, "{\"node_id\":0,\"result\":-7}"); /* CHAT_EVT_CONNECT_RESULT */ return; } struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!inst) { bridge_log(BLEV_ERROR, "join: instance not running"); utun_bridge_event(6, "{\"node_id\":0,\"result\":-7}"); /* CHAT_EVT_CONNECT_RESULT */ return; } chat_sync_connect_from_invite(inst, channel_id, real_node_id, pubkey_bin, addrs_data, addr_count, addrs_data_len, NULL, join_key); } void utun_bridge_request_invite_candidates(const char* channel_id) { if (!channel_id || !channel_id[0]) { bridge_log(BLEV_ERROR, "request_invite_candidates: null ch_id"); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "request_invite_candidates: uasync not running"); return; } struct chat_invite_cand_req* req = u_calloc(1, sizeof(*req)); if (!req) { bridge_log(BLEV_ERROR, "request_invite_candidates: OOM"); return; } req->inst = instance_lite_get_instance(); snprintf(req->ch_id, sizeof(req->ch_id), "%s", channel_id); bridge_log(BLEV_INFO, "request_invite_candidates ch=%s", channel_id); uasync_post(ua, chat_invite_candidates_trampoline, req); } void utun_bridge_request_invite_link(const char* channel_id, uint64_t target_node_id, uint64_t request_id) { if (!channel_id || !channel_id[0]) { bridge_log(BLEV_ERROR, "request_invite_link: null ch_id"); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "request_invite_link: uasync not running"); return; } struct chat_invite_build_req* req = u_calloc(1, sizeof(*req)); if (!req) { bridge_log(BLEV_ERROR, "request_invite_link: OOM"); return; } req->inst = instance_lite_get_instance(); strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1); req->request_id = request_id; req->target_node_id = target_node_id; bridge_log(BLEV_INFO, "request_invite_link ch=%s target=0x%016llx", channel_id, (unsigned long long)target_node_id); uasync_post(ua, chat_invite_build_link_trampoline, req); } void utun_bridge_connect_channel(const char* channel_id) { if (!channel_id || !channel_id[0]) { bridge_log(BLEV_ERROR, "connect_channel: null ch_id"); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) return; struct chat_create_auto_arg* req = u_calloc(1, sizeof(*req)); if (!req) return; req->inst = instance_lite_get_instance(); snprintf(req->name, sizeof(req->name), "%s", channel_id); bridge_log(BLEV_INFO, "connect_channel ch=%s", channel_id); uasync_post(ua, chat_core_connect_channel_trampoline, req); } void utun_bridge_connect_member_node(const char* channel_id, uint64_t node_id) { if (!channel_id || !channel_id[0] || node_id == 0) { bridge_log(BLEV_ERROR, "connect_member_node: invalid args ch=%s node=0x%016llx", channel_id ? channel_id : "(null)", (unsigned long long)node_id); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) return; struct chat_connect_node_req* req = u_calloc(1, sizeof(*req)); if (!req) { bridge_log(BLEV_ERROR, "connect_member_node: OOM"); return; } req->inst = instance_lite_get_instance(); strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1); req->node_id = node_id; bridge_log(BLEV_INFO, "connect_member_node ch=%s node=0x%016llx", channel_id, (unsigned long long)node_id); uasync_post(ua, chat_core_connect_node_trampoline, req); } static char* json_escape_alloc(const char* str) { if (!str) return u_strdup(""); size_t len = strlen(str), cap = len * 2 + 8; char* out = u_malloc(cap); if (!out) return u_strdup(""); size_t pos = 0; for (size_t i = 0; i < len && pos < cap - 2; i++) { char c = str[i]; if (c == '"' || c == '\\') { out[pos++] = '\\'; out[pos++] = c; } else if (c == '\n') { out[pos++] = '\\'; out[pos++] = 'n'; } else if (c == '\r') { out[pos++] = '\\'; out[pos++] = 'r'; } else if (c == '\t') { out[pos++] = '\\'; out[pos++] = 't'; } else if ((unsigned char)c < 32) { pos += snprintf(out + pos, cap - pos, "\\u%04x", (unsigned char)c); } else out[pos++] = c; } out[pos] = '\0'; return out; } static void bridge_get_node_name(sqlite3* db, uint64_t node_id, const char* channel_id, char* out, size_t sz) { out[0] = '\0'; if (!db || node_id == 0) return; sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); if (sqlite3_step(st) == SQLITE_ROW) { const char* n = (const char*)sqlite3_column_text(st, 0); if (n && n[0]) { snprintf(out, sz, "%s", n); sqlite3_finalize(st); return; } } sqlite3_finalize(st); } if (!channel_id || !channel_id[0]) return; char peers_tbl[80], sql[256]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); snprintf(sql, sizeof(sql), "SELECT userinfo FROM \"%s\" WHERE node_id=?", peers_tbl); if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return; sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); if (sqlite3_step(st) == SQLITE_ROW) { const char* ui = (const char*)sqlite3_column_text(st, 0); if (ui && ui[0]) { const char* ns = strstr(ui, "\"name\":\""); if (ns) { ns += 8; const char* ne = strchr(ns, '"'); if (ne && ne > ns) { size_t nl = (size_t)(ne - ns); if (nl >= sz) nl = sz - 1; memcpy(out, ns, nl); out[nl] = '\0'; } } } } sqlite3_finalize(st); } /* Формирует человекочитаемое тело превью последнего сообщения по content_type и "d"-полю. */ static void bridge_format_preview_body(const char* ct, const char* d, char* out, size_t out_sz) { int is_text = (!ct || !ct[0] || strcmp(ct, "text/plain") == 0 || strcmp(ct, "voice_transcription") == 0); if (is_text) { size_t tl = strlen(d); if (tl > 60) tl = 60; if (tl >= out_sz) tl = out_sz - 1; memcpy(out, d, tl); out[tl] = '\0'; return; } if (strcmp(ct, "audio/opus") == 0) { int dur_ms = 0; if (d[0] == '*') { const char* pipe = strchr(d, '|'); size_t b64len = pipe ? (size_t)(pipe - d - 1) : strlen(d) - 1; uint8_t dec[256]; size_t declen = b64_decode(d + 1, b64len, dec, sizeof(dec)); if (declen > 0) { char raw[256]; if (declen >= sizeof(raw)) declen = sizeof(raw) - 1; memcpy(raw, dec, declen); raw[declen] = '\0'; const char* dur = strstr(raw, ";dur="); if (dur) dur_ms = atoi(dur + 5); } } if (dur_ms > 0) { int s = dur_ms / 1000; snprintf(out, out_sz, "Voice message (%d:%02d)", s / 60, s % 60); } else { snprintf(out, out_sz, "Voice message"); } return; } if (strncmp(ct, "video/", 6) == 0) { snprintf(out, out_sz, "Video"); return; } if (strncmp(ct, "image/", 6) == 0) { snprintf(out, out_sz, "Photo"); return; } /* файл и прочие нетекстовые: декодируем base64-префикс в имя файла */ if (d[0] == '*') { const char* pipe = strchr(d, '|'); size_t b64len = pipe ? (size_t)(pipe - d - 1) : strlen(d) - 1; uint8_t dec[512]; size_t declen = b64_decode(d + 1, b64len, dec, sizeof(dec)); if (declen > 0) { if (declen >= out_sz) declen = out_sz - 1; memcpy(out, dec, declen); out[declen] = '\0'; return; } } snprintf(out, out_sz, "File"); } char* utun_bridge_get_channels_json(void) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); sqlite3* db = chat_core_get_db(inst); if (!chat_core_is_initialized(inst) || !db) { bridge_log(BLEV_WARN, "get_channels: not initialized db=%p initialized=%d", (void*)db, chat_core_is_initialized(inst)); return u_strdup("[]"); } size_t cap = 4096, pos = 1; char* json = u_malloc(cap); if (!json) return u_strdup("[]"); json[0] = '['; sqlite3_stmt* st = NULL; int rc = sqlite3_prepare_v2(db, "SELECT channel_id, name, last_msg_at FROM channels ORDER BY last_msg_at DESC, name ASC", -1, &st, NULL); if (rc != SQLITE_OK) { u_free(json); return u_strdup("[]"); } int first = 1; while (sqlite3_step(st) == SQLITE_ROW) { const char* ch_id = (const char*)sqlite3_column_text(st, 0); const char* name = (const char*)sqlite3_column_text(st, 1); int64_t last_msg_at = sqlite3_column_int64(st, 2); if (!ch_id) continue; char tbl_msg[80], tbl_peers[80], last_msg[160]; msg_table_name(ch_id, tbl_msg, sizeof(tbl_msg)); peers_table_name(ch_id, tbl_peers, sizeof(tbl_peers)); last_msg[0] = '\0'; sqlite3_stmt* lm = NULL; char sql[256]; snprintf(sql, sizeof(sql), "SELECT node_id, data FROM \"%s\" ORDER BY id DESC LIMIT 1", tbl_msg); if (sqlite3_prepare_v2(db, sql, -1, &lm, NULL) == SQLITE_OK) { if (sqlite3_step(lm) == SQLITE_ROW) { int64_t author = sqlite3_column_int64(lm, 0); const uint8_t* dptr = sqlite3_column_blob(lm, 1); int dlen = sqlite3_column_bytes(lm, 1); char body[160] = ""; if (dptr && dlen > 0) { const char* dp = (const char*)dptr; char ct[32] = "text/plain"; const char* cts = strstr(dp, "\"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'; } } } const char* ds = strstr(dp, "\"d\":\""); if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 511) tl = 511; char d[512]; memcpy(d, ds, tl); d[tl] = '\0'; bridge_format_preview_body(ct, d, body, sizeof(body)); } } } char author_name[128]; bridge_get_node_name(db, (uint64_t)author, ch_id, author_name, sizeof(author_name)); if (!author_name[0]) snprintf(author_name, sizeof(author_name), "0x%016llx", (unsigned long long)author); snprintf(last_msg, sizeof(last_msg), "%s: %s", author_name, body); } sqlite3_finalize(lm); } int online = 0; uint64_t my_id = inst->node_id; sqlite3_stmt* pc = NULL; snprintf(sql, sizeof(sql), "SELECT DISTINCT node_id FROM \"%s\"", tbl_peers); if (sqlite3_prepare_v2(db, sql, -1, &pc, NULL) == SQLITE_OK) { while (sqlite3_step(pc) == SQLITE_ROW) { uint64_t nid = (uint64_t)sqlite3_column_int64(pc, 0); if (nid == my_id) continue; if (inst) { struct ETCP_CONN* c = instance_find_conn(inst, nid); if (c && c->initialized && c->links_up) online++; } else online++; } sqlite3_finalize(pc); } char* esc_name = json_escape_alloc(name ? name : ch_id); char* esc_msg = json_escape_alloc(last_msg); const char* sep = first ? "" : ","; first = 0; size_t needed = snprintf(NULL, 0, "%s{\"id\":\"%s\",\"name\":\"%s\",\"lastMsgAt\":%lld,\"lastMsg\":\"%s\",\"peersOnline\":%d}", sep, ch_id, esc_name, (long long)last_msg_at, esc_msg, online); 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\":\"%s\",\"name\":\"%s\",\"lastMsgAt\":%lld,\"lastMsg\":\"%s\",\"peersOnline\":%d}", sep, ch_id, esc_name, (long long)last_msg_at, esc_msg, online); u_free(esc_msg); u_free(esc_name); } sqlite3_finalize(st); json[pos++] = ']'; json[pos] = '\0'; return json; } 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[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," "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) { 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; } 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) { 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 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)); } 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, ",\"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); 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) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); return (inst && chat_core_is_initialized(inst)) ? inst->node_id : 0; } /* ── DM (личные сообщения) ── */ void utun_bridge_dm_start(uint64_t node_id, const char* source_ch_id, const char* peer_name) { if (!node_id) { bridge_log(BLEV_ERROR, "dm_start: invalid node_id=0"); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "dm_start: uasync not running"); return; } struct dm_start_req* req = u_calloc(1, sizeof(*req)); if (!req) { bridge_log(BLEV_ERROR, "dm_start: OOM"); return; } req->inst = instance_lite_get_instance(); req->peer_node_id = node_id; if (source_ch_id) strncpy(req->source_ch_id, source_ch_id, sizeof(req->source_ch_id) - 1); if (peer_name) strncpy(req->peer_name, peer_name, sizeof(req->peer_name) - 1); bridge_log(BLEV_INFO, "dm_start queued peer=0x%016llx ch=%s", (unsigned long long)node_id, source_ch_id ? source_ch_id : ""); uasync_post(ua, dm_start_trampoline, req); } void utun_bridge_dm_send(const char* conv_id, const char* text) { if (!conv_id || !conv_id[0] || !text) { bridge_log(BLEV_ERROR, "dm_send: invalid args"); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "dm_send: uasync not running"); return; } size_t len = strlen(text); struct dm_send_req* req = u_calloc(1, sizeof(struct dm_send_req) + len); if (!req) { bridge_log(BLEV_ERROR, "dm_send: OOM"); return; } req->inst = instance_lite_get_instance(); strncpy(req->conv_id, conv_id, sizeof(req->conv_id) - 1); req->data_len = (uint32_t)len; req->data = (uint8_t*)(req + 1); memcpy(req->data, text, len); bridge_log(BLEV_DEBUG, "dm_send queued conv=%s len=%zu", conv_id, len); uasync_post(ua, dm_send_trampoline, req); } void utun_bridge_dm_request_conversations(void) { struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) { bridge_log(BLEV_WARN, "DM conversations: core stopped"); return; } struct dm_conversations_req* req = u_calloc(1, sizeof(*req)); if (!req) { bridge_log(BLEV_ERROR, "DM conversations: OOM"); return; } req->inst = instance_lite_get_instance(); uasync_post(ua, dm_conversations_trampoline, req); } void utun_bridge_dm_request_messages(const char* conv_id) { struct UASYNC* ua = instance_lite_get_uasync(); if (!conv_id || !conv_id[0] || !uasync_is_running(ua)) { bridge_log(BLEV_WARN, "DM history: invalid request/core stopped"); return; } struct dm_messages_req* req = u_calloc(1, sizeof(*req)); if (!req) { bridge_log(BLEV_ERROR, "DM history: OOM"); return; } req->inst = instance_lite_get_instance(); snprintf(req->conv_id, sizeof(req->conv_id), "%s", conv_id); uasync_post(ua, dm_messages_trampoline, req); } int utun_bridge_dm_send_file(const char* conv_id, const char* path, const char* name) { struct UASYNC* ua = instance_lite_get_uasync(); if (!conv_id || !path || !name || !uasync_is_running(ua)) { bridge_log(BLEV_WARN, "DM file: invalid request/core stopped"); return -1; } struct dm_file_req* req = u_calloc(1, sizeof(*req)); if (!req) { bridge_log(BLEV_ERROR, "DM file: OOM"); return -1; } if (strlen(conv_id) >= sizeof(req->conv) || strlen(path) >= sizeof(req->path) || strlen(name) >= sizeof(req->info.name)) { bridge_log(BLEV_ERROR, "DM file: path, conversation or name too long"); u_free(req); return -1; } req->inst = instance_lite_get_instance(); snprintf(req->conv, sizeof(req->conv), "%s", conv_id); snprintf(req->path, sizeof(req->path), "%s", path); req->info.kind = ATTACHMENT_FILE; req->remove_source = 1; /* PM picker передаёт собственный уникальный cache-файл. */ snprintf(req->info.name, sizeof(req->info.name), "%s", name); if (attachment_validate(&req->info)) { bridge_log(BLEV_ERROR, "DM file: invalid name"); u_free(req); return -1; } struct posted_task* post = u_calloc(1, sizeof(*post)); if (!post) { bridge_log(BLEV_ERROR, "DM file: completion allocation failed"); u_free(req); return -1; } post->callback = dm_send_file_trampoline; post->arg = req; uasync_post_reserved(ua, post); return 0; } void utun_bridge_set_debug_level(const char* category, const char* level) { if (!level) return; if (!category || !category[0]) { debug_level_t lvl = debug_level_from_name(level); debug_set_level(lvl); } else { debug_set_category_level_by_name(category, level); } } void utun_bridge_chat_setting_set(const char* name, const char* value) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!inst) { bridge_log(BLEV_ERROR, "chat_setting_set: instance not running"); return; } chat_core_set_setting(inst, name, value); } int utun_bridge_chat_setting_get_int(const char* name, int def) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); return chat_setting_get_int(inst, name, def); } /* ────────────────────────────────────────────────────────────────── * Channel members (queried from shared DB) * ────────────────────────────────────────────────────────────────── */ #include "../../../src/routing_layer/topo_node_sqlite.h" #include "../../../lib/json_flat.h" #include char* utun_bridge_get_channel_members_json(const char* channel_id) { 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 peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); sqlite3_stmt* cnt_st = NULL; char sql[512]; snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM \"%s\"", peers_tbl); /* if table doesn't exist, return empty */ if (sqlite3_prepare_v2(db, sql, -1, &cnt_st, NULL) != SQLITE_OK) return u_strdup("[]"); int member_count = 0; if (sqlite3_step(cnt_st) == SQLITE_ROW) member_count = sqlite3_column_int(cnt_st, 0); sqlite3_finalize(cnt_st); if (member_count == 0) return u_strdup("[]"); snprintf(sql, sizeof(sql), "SELECT p.node_id, COALESCE(n.name,'')," " COALESCE(p.local_nick,''), COALESCE(p.adm_tags,'')," " COALESCE(p.node_type,0), COALESCE(p.node_RTT,0), COALESCE(p.storage,0)," " COALESCE(p.userinfo,''),p.source,length(p.adm_tags_sig),COALESCE(p.deleted,0),p.radio_muted" " FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id" " ORDER BY p.node_id ASC", peers_tbl); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) { bridge_log(BLEV_ERROR, "get_channel_members: main prepare failed ch=%s err=%s", channel_id, sqlite3_errmsg(db)); return u_strdup("[]"); } size_t cap = 4096, pos = 1; char* json = u_malloc(cap); if (!json) { sqlite3_finalize(st); return u_strdup("[]"); } json[0] = '['; uint64_t my_id = inst->node_id; int first = 1; while (sqlite3_step(st) == SQLITE_ROW) { uint64_t node_id = (uint64_t)sqlite3_column_int64(st, 0); const char* name = (const char*)sqlite3_column_text(st, 1); const char* local_nick = (const char*)sqlite3_column_text(st, 2); const char* adm_tags = (const char*)sqlite3_column_text(st, 3); int node_type = sqlite3_column_int(st, 4); int node_rtt = sqlite3_column_int(st, 5); int storage_flag = sqlite3_column_int(st, 6); int is_admin = 0, is_super = 0, is_moder = 0, is_verified = 0, is_deleted = sqlite3_column_int(st, 10); int storage_role = 0; if (adm_tags) { char buf[256]; if (json_flat_get(adm_tags, "supernode", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_super = 1; if (json_flat_get(adm_tags, "admin", buf, sizeof(buf)) == 0) { const char* last = buf; for (const char* p = buf; *p; p++) if (*p == 'e' || *p == 'd') last = p; if (*last == 'e') is_admin = 1; } if (json_flat_get(adm_tags, "moder", buf, sizeof(buf)) == 0) { const char* last = buf; for (const char* p = buf; *p; p++) if (*p == 'e' || *p == 'd') last = p; if (*last == 'e') is_moder = 1; } if (json_flat_get(adm_tags, "verified", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_verified = 1; if (json_flat_get(adm_tags, "deleted", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_deleted = 1; if (!is_deleted && sqlite3_column_int(st, 8) == 0 && sqlite3_column_int(st, 9) == 64 && json_flat_get(adm_tags, "storage", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) storage_role = 1; } char* esc_name = json_escape_alloc(local_nick && local_nick[0] ? local_nick : (name && name[0] ? name : "")); char* esc_userinfo = json_escape_alloc((const char*)sqlite3_column_text(st, 7)); if (!esc_name || !esc_userinfo) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "Android members: JSON allocation failed channel=%s node=%016llx", channel_id, (unsigned long long)node_id); u_free(esc_name); u_free(esc_userinfo); continue; } const char* sep = first ? "" : ","; first = 0; size_t needed = snprintf(NULL, 0, "%s{\"n\":\"%s\",\"ni\":%llu,\"on\":0,\"ad\":%d,\"sp\":%d,\"md\":%d,\"st\":%d," "\"vd\":%d,\"dl\":%d,\"tp\":%d,\"rt\":%d,\"is\":%d,\"ui\":\"%s\",\"sr\":%d,\"rm\":%d}", sep, esc_name, (unsigned long long)node_id, is_admin, is_super, is_moder, storage_flag, is_verified, is_deleted, node_type, node_rtt, (node_id == my_id) ? 1 : 0, esc_userinfo, storage_role, sqlite3_column_int(st, 11)); 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{\"n\":\"%s\",\"ni\":%llu,\"on\":0,\"ad\":%d,\"sp\":%d,\"md\":%d,\"st\":%d," "\"vd\":%d,\"dl\":%d,\"tp\":%d,\"rt\":%d,\"is\":%d,\"ui\":\"%s\",\"sr\":%d,\"rm\":%d}", sep, esc_name, (unsigned long long)node_id, is_admin, is_super, is_moder, storage_flag, is_verified, is_deleted, node_type, node_rtt, (node_id == my_id) ? 1 : 0, esc_userinfo, storage_role, sqlite3_column_int(st, 11)); u_free(esc_userinfo); u_free(esc_name); } sqlite3_finalize(st); json[pos++] = ']'; json[pos] = '\0'; return json; } /* Не продолжаем сборку JSON после усечения строки: pos должен оставаться внутри буфера. */ static int member_detail_append(char* buf, size_t size, size_t* pos, const char* fmt, ...) { va_list ap; va_start(ap, fmt); int written = vsnprintf(buf + *pos, size - *pos, fmt, ap); va_end(ap); if (written < 0 || (size_t)written >= size - *pos) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "member detail: JSON append failed used=%zu capacity=%zu required=%d", *pos, size, written); return -1; } *pos += (size_t)written; return 0; } char* utun_bridge_get_member_detail_json(const char* channel_id, uint64_t node_id) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); sqlite3* db = chat_core_get_db(inst); if (!chat_core_is_initialized(inst) || !db || !channel_id || node_id == 0) return u_strdup("{}"); char buf[8192]; size_t pos = 0; pos += (size_t)snprintf(buf + pos, sizeof(buf) - pos, "{\"ni\":%llu", (unsigned long long)node_id); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, "SELECT COALESCE(name,'')," " COALESCE(last_seen_at,0), COALESCE(created_at,0)" " FROM nodes WHERE node_id=?", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); if (sqlite3_step(st) == SQLITE_ROW) { const char* name = (const char*)sqlite3_column_text(st, 0); int64_t last_seen = sqlite3_column_int64(st, 1); int64_t created = sqlite3_column_int64(st, 2); char* esc_name = json_escape_alloc(name && name[0] ? name : ""); int rc = member_detail_append(buf, sizeof(buf), &pos, ",\"n\":\"%s\",\"on\":0,\"ls\":%lld,\"cr\":%lld", esc_name, (long long)last_seen, (long long)created); u_free(esc_name); if (rc < 0) { sqlite3_finalize(st); return u_strdup("{}"); } } sqlite3_finalize(st); } char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); /* Родитель дерева приглашений относится к каналу; без подписи это ещё неизвестная связь. */ char sql[512]; snprintf(sql, sizeof(sql), "SELECT p.signed_by, length(p.signature), p.source, COALESCE(NULLIF(i.local_nick,''),n.name,'')" " FROM \"%s\" p LEFT JOIN \"%s\" i ON i.node_id=p.signed_by" " LEFT JOIN nodes n ON n.node_id=p.signed_by WHERE p.node_id=?", peers_tbl, peers_tbl); int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); if (rc == SQLITE_OK) rc = sqlite3_step(st); if (rc == SQLITE_ROW) { int known = sqlite3_column_type(st, 0) != SQLITE_NULL && sqlite3_column_int(st, 1) == 64 && sqlite3_column_int(st, 2) == 0; uint64_t inviter_id = (uint64_t)sqlite3_column_int64(st, 0); const char* inviter_name = known && inviter_id ? (const char*)sqlite3_column_text(st, 3) : ""; char* escaped = json_escape_alloc(inviter_name); if (!escaped) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "member invitation: name allocation failed ch=%s node=%016llx", channel_id, (unsigned long long)node_id); sqlite3_finalize(st); return u_strdup("{}"); } char inviter_json[32] = "null"; if (known) snprintf(inviter_json, sizeof(inviter_json), "%llu", (unsigned long long)inviter_id); int append_rc = member_detail_append(buf, sizeof(buf), &pos, ",\"inviter_id\":%s,\"inviter_name\":\"%s\"", inviter_json, escaped); u_free(escaped); if (append_rc < 0) { sqlite3_finalize(st); return u_strdup("{}"); } DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "member invitation: ch=%s node=%016llx known=%d inviter=%016llx name=%s", channel_id, (unsigned long long)node_id, known, (unsigned long long)inviter_id, inviter_name); } else { DEBUG_WARN(DEBUG_CATEGORY_CHAT, "member invitation: read failed ch=%s node=%016llx rc=%d: %s", channel_id, (unsigned long long)node_id, rc, sqlite3_errmsg(db)); } sqlite3_finalize(st); st = NULL; if (member_detail_append(buf, sizeof(buf), &pos, ",\"adr\":[") < 0) return u_strdup("{}"); if (sqlite3_prepare_v2(db, "SELECT family, protocol, address, port, socket_id, addr_type, COALESCE(rtt,0)" " FROM node_addresses WHERE node_id=? ORDER BY socket_id, addr_type, family, port", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); int af = 1; while (sqlite3_step(st) == SQLITE_ROW) { int family = sqlite3_column_int(st, 0); int proto = sqlite3_column_int(st, 1); const uint8_t* addr = (const uint8_t*)sqlite3_column_blob(st, 2); int addr_len = sqlite3_column_bytes(st, 2); int port = sqlite3_column_int(st, 3); int sid = sqlite3_column_int(st, 4); int at = sqlite3_column_int(st, 5); int rtt = sqlite3_column_int(st, 6); char addr_str[64] = ""; if (addr && addr_len >= (family == 4 ? 4 : 16)) { if (family == 4) snprintf(addr_str, sizeof(addr_str), "%d.%d.%d.%d", addr[0], addr[1], addr[2], addr[3]); else if (family == 6) { char tmp[INET6_ADDRSTRLEN]; inet_ntop(AF_INET6, addr, tmp, sizeof(tmp)); snprintf(addr_str, sizeof(addr_str), "%s", tmp); } } const char* pn; if (proto & 0x04) pn = "TCP+REALITY"; else switch (proto) { case 1: pn = "UDP"; break; case 2: pn = "TCP"; break; default: pn = "UDP/TCP"; break; } if (member_detail_append(buf, sizeof(buf), &pos, "%s{\"fm\":%d,\"pt\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"si\":%d,\"at\":%d,\"rt\":%d}", af ? "" : ",", family, pn, addr_str[0] ? addr_str : "?", port, sid, at, rtt) < 0) { sqlite3_finalize(st); return u_strdup("{}"); } af = 0; } sqlite3_finalize(st); } if (member_detail_append(buf, sizeof(buf), &pos, "]}") < 0) return u_strdup("{}"); return u_strdup(buf); } int utun_bridge_toggle_member_radio_mute(const char* channel_id, uint64_t node_id) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); struct UASYNC* ua = instance_lite_get_uasync(); if (!inst || !uasync_is_running(ua) || !channel_id || !channel_id[0] || strlen(channel_id) >= 64 || !node_id) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: Android mute request rejected: core unavailable or invalid member"); return -1; } struct chat_member_radio_mute_req* req = u_calloc(1, sizeof(*req)); struct posted_task* post = u_calloc(1, sizeof(*post)); if (!req || !post) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: Android mute request allocation failed"); u_free(req); u_free(post); return -1; } req->inst = inst; req->node_id = node_id; strcpy(req->ch_id, channel_id); post->callback = chat_core_toggle_member_radio_mute_trampoline; post->arg = req; uasync_post_reserved(ua, post); return 0; } void utun_bridge_set_member_nick(const char* channel_id, uint64_t node_id, const char* nick) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); sqlite3* db = chat_core_get_db(inst); if (!chat_core_is_initialized(inst) || !db || !channel_id || node_id == 0) return; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[256]; snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET local_nick=? WHERE node_id=?", peers_tbl); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK) { if (nick && nick[0]) sqlite3_bind_text(st, 1, nick, -1, SQLITE_STATIC); else sqlite3_bind_null(st, 1); sqlite3_bind_int64(st, 2, (sqlite3_int64)node_id); sqlite3_step(st); sqlite3_finalize(st); } bridge_log(BLEV_DEBUG, "set_member_nick ch=%s nid=%llu nick=%s", channel_id, (unsigned long long)node_id, nick ? nick : "(null)"); } struct member_tags_edit_req { struct UTUN_INSTANCE* inst; char ch_id[64]; uint64_t node_id; int admin, supernode, moder, storage, verified, deleted; }; static void member_tags_edit_trampoline(void* arg) { struct member_tags_edit_req* req = (struct member_tags_edit_req*)arg; if (!req || !req->inst) { u_free(arg); return; } struct chat_member_tags* t = chat_member_tags_load(req->inst, req->ch_id, req->node_id); if (!t) { bridge_log(BLEV_ERROR, "set_member_flags: tags_load failed ch=%s nid=0x%016llx", req->ch_id, (unsigned long long)req->node_id); u_free(arg); return; } chat_member_tags_set(t, "admin", req->admin ? "yes" : "no"); chat_member_tags_set(t, "moder", req->moder ? "yes" : "no"); chat_member_tags_set(t, "supernode", req->supernode ? "yes" : "no"); chat_member_tags_set(t, "storage", req->storage ? "yes" : "no"); chat_member_tags_set(t, "verified", req->verified ? "yes" : "no"); chat_member_tags_set(t, "deleted", req->deleted ? "yes" : "no"); int rc = chat_member_tags_commit(req->inst, t); bridge_log(rc == 0 ? BLEV_INFO : BLEV_ERROR, "set_member_flags ch=%s nid=0x%016llx admin=%d super=%d moder=%d storage=%d verified=%d deleted=%d rc=%d", req->ch_id, (unsigned long long)req->node_id, req->admin, req->supernode, req->moder, req->storage, req->verified, req->deleted, rc); chat_member_tags_free(t); u_free(arg); } int utun_bridge_is_channel_owner(const char* channel_id) { 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 0; uint8_t priv[32]; return topo_node_sqlite_channel_get_priv(db, channel_id, priv) == 0 ? 1 : 0; } void utun_bridge_set_member_flags(const char* channel_id, uint64_t node_id, int admin, int supernode, int moder, int storage_flag, int verified, int deleted) { if (!channel_id || node_id == 0) { bridge_log(BLEV_ERROR, "set_member_flags: invalid args"); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "set_member_flags: uasync not running"); return; } struct member_tags_edit_req* req = u_calloc(1, sizeof(*req)); if (!req) { bridge_log(BLEV_ERROR, "set_member_flags: OOM"); return; } req->inst = instance_lite_get_instance(); strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1); req->node_id = node_id; req->admin = admin; req->supernode = supernode; req->moder = moder; req->storage = storage_flag; req->verified = verified; req->deleted = deleted; uasync_post(ua, member_tags_edit_trampoline, req); bridge_log(BLEV_DEBUG, "set_member_flags queued ch=%s nid=0x%016llx admin=%d super=%d moder=%d storage=%d verified=%d deleted=%d", channel_id, (unsigned long long)node_id, admin, supernode, moder, storage_flag, verified, deleted); } char* utun_bridge_get_member_links_json(uint64_t node_id) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!inst || node_id == 0) return u_strdup("{}"); struct ETCP_CONN* conn = instance_find_conn(inst, node_id); if (!conn) return u_strdup("{}"); char buf[4096]; int pos = snprintf(buf, sizeof(buf), "{\"cn\":true,\"in\":%s,\"lu\":%d,\"lk\":[", conn->initialized ? "true" : "false", conn->links_up); int first = 1; for (struct ETCP_LINK* lk = conn->links; lk; lk = lk->next) { if (pos >= (int)sizeof(buf) - 100) break; char la[INET6_ADDRSTRLEN] = ""; int lf = 0; uint16_t lp = 0; if (lk->conn) { const struct sockaddr_storage* lsa = lk->conn->interface_addr.ss_family ? &lk->conn->interface_addr : &lk->conn->local_addr; if (lsa->ss_family == AF_INET) { inet_ntop(AF_INET, &((struct sockaddr_in*)lsa)->sin_addr, la, sizeof(la)); lf = 4; lp = ntohs(((struct sockaddr_in*)lsa)->sin_port); } else if (lsa->ss_family == AF_INET6) { inet_ntop(AF_INET6, &((struct sockaddr_in6*)lsa)->sin6_addr, la, sizeof(la)); lf = 6; lp = ntohs(((struct sockaddr_in6*)lsa)->sin6_port); } } else if (lk->is_tcp && lk->tcp_link) { const struct sockaddr_storage* lsa = stcp_link_get_local_addr(lk->tcp_link); if (lsa && lsa->ss_family == AF_INET) { inet_ntop(AF_INET, &((struct sockaddr_in*)lsa)->sin_addr, la, sizeof(la)); lf = 4; lp = ntohs(((struct sockaddr_in*)lsa)->sin_port); } else if (lsa && lsa->ss_family == AF_INET6) { inet_ntop(AF_INET6, &((struct sockaddr_in6*)lsa)->sin6_addr, la, sizeof(la)); lf = 6; lp = ntohs(((struct sockaddr_in6*)lsa)->sin6_port); } } char ra[INET6_ADDRSTRLEN] = ""; int rf = 0; uint16_t rp = 0; int st = lk->link_state, ls = lk->link_status; if (lk->is_tcp && lk->tcp_link && stcp_link_is_ready(lk->tcp_link)) { st = 3; ls = 1; } const struct sockaddr_storage* rsa = &lk->remote_addr; if (rsa->ss_family == AF_INET) { inet_ntop(AF_INET, &((struct sockaddr_in*)rsa)->sin_addr, ra, sizeof(ra)); rf = 4; rp = ntohs(((struct sockaddr_in*)rsa)->sin_port); } else if (rsa->ss_family == AF_INET6) { inet_ntop(AF_INET6, &((struct sockaddr_in6*)rsa)->sin6_addr, ra, sizeof(ra)); rf = 6; rp = ntohs(((struct sockaddr_in6*)rsa)->sin6_port); } const char* sep = first ? "" : ","; first = 0; const char* sn = lk->conn ? lk->conn->name : ""; pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos, "%s{\"id\":%d,\"pt\":\"%s\",\"lf\":%d,\"la\":\"%s\",\"lp\":%d,\"rf\":%d,\"ra\":\"%s\",\"rp\":%d,\"st\":%d,\"ls\":%d,\"is\":%d,\"sn\":\"%s\"}", sep, lk->local_link_id, lk->reality_set ? "TCP+REALITY" : (lk->is_tcp ? "TCP" : "UDP"), lf, la, (int)lp, rf, ra, (int)rp, st, ls, lk->is_server, sn); } if (pos < (int)sizeof(buf)) pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos, "]}"); bridge_log(BLEV_INFO, "getMemberLinks node=0x%016llx → %s", (unsigned long long)node_id, buf); return u_strdup(buf); } /* ────────────────────────────────────────────────────────────────── * Передача прав админа (канальный приватный ключ) другому узлу * ────────────────────────────────────────────────────────────────── */ void utun_bridge_transfer_admin(const char* channel_id, uint64_t node_id) { if (!channel_id || node_id == 0) { bridge_log(BLEV_ERROR, "transfer_admin: invalid args"); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "transfer_admin: uasync not running"); return; } struct chat_transfer_admin_req* req = u_calloc(1, sizeof(*req)); if (!req) { bridge_log(BLEV_ERROR, "transfer_admin: OOM"); return; } req->inst = instance_lite_get_instance(); strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1); req->target_node_id = node_id; uasync_post(ua, chat_core_transfer_admin_trampoline, req); bridge_log(BLEV_INFO, "transfer_admin queued ch=%s → 0x%016llx", channel_id, (unsigned long long)node_id); } /* ────────────────────────────────────────────────────────────────── * Удаление канала (локальное) * ────────────────────────────────────────────────────────────────── */ void utun_bridge_delete_channel(const char* channel_id) { if (!channel_id || !channel_id[0]) { bridge_log(BLEV_ERROR, "delete_channel: empty ch_id"); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "delete_channel: uasync not running"); return; } struct chat_delete_channel_req* req = u_calloc(1, sizeof(*req)); if (!req) { bridge_log(BLEV_ERROR, "delete_channel: OOM"); return; } req->inst = instance_lite_get_instance(); strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1); uasync_post(ua, chat_core_delete_channel_trampoline, req); bridge_log(BLEV_INFO, "delete_channel queued ch=%s", channel_id); } /* ── Per-channel «play sound on message» ── */ struct sound_setting_req { struct UTUN_INSTANCE* inst; char ch_id[64]; int on; }; static void sound_setting_trampoline(void* arg) { struct sound_setting_req* req = (struct sound_setting_req*)arg; if (!req || !req->inst) { u_free(arg); return; } chat_core_set_sound_on_message(req->inst, req->ch_id, req->on); u_free(arg); } int utun_bridge_get_sound_on_message(const char* channel_id) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!inst || !chat_core_is_initialized(inst) || !channel_id || !channel_id[0]) return 1; return chat_core_get_sound_on_message(inst, channel_id); } void utun_bridge_set_sound_on_message(const char* channel_id, int on) { if (!channel_id || !channel_id[0]) { bridge_log(BLEV_ERROR, "set_sound: empty ch_id"); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "set_sound: uasync not running"); return; } struct sound_setting_req* req = u_calloc(1, sizeof(*req)); if (!req) { bridge_log(BLEV_ERROR, "set_sound: OOM"); return; } req->inst = instance_lite_get_instance(); strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1); req->on = on ? 1 : 0; uasync_post(ua, sound_setting_trampoline, req); bridge_log(BLEV_INFO, "set_sound queued ch=%s on=%d", channel_id, on ? 1 : 0); } /* ────────────────────────────────────────────────────────────────── * Local sockets state (for auto-mode UI display) * ────────────────────────────────────────────────────────────────── */ #include "../../../lib/socket_compat.h" char* utun_bridge_get_local_sockets_json(void) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!inst || !chat_core_is_initialized(inst)) return u_strdup("[]"); size_t cap = 4096, pos = 1; char* json = u_malloc(cap); if (!json) return u_strdup("[]"); json[0] = '['; int first = 1; bridge_log(BLEV_INFO, "getLocalSockets: etcp_sockets=%p", (void*)inst->etcp_sockets); /* Helper: count per-socket links_up / links_down */ #define SOCK_COUNT_LINKS(_s, _lu, _ld) do { \ int *_p_lu = &(_lu), *_p_ld = &(_ld); \ *_p_lu = 0; *_p_ld = 0; \ if (inst->connections) { \ for (struct ll_entry* _e = inst->connections->head; _e; _e = _e->next) { \ struct conn_queue_entry* _ce = (struct conn_queue_entry*)_e->data; \ if (!_ce || !_ce->conn) continue; \ for (struct ETCP_LINK* _lk = _ce->conn->links; _lk; _lk = _lk->next) { \ if (_lk->conn != (_s)) continue; \ if (_lk->link_state == 3) (*_p_lu)++; else (*_p_ld)++; \ } \ } \ } \ if (inst->tcp_connections) { \ for (struct ll_entry* _e = inst->tcp_connections->head; _e; _e = _e->next) { \ struct tcp_conn_entry* _te = (struct tcp_conn_entry*)_e->data; \ if (!_te || !_te->etcp_conn) continue; \ for (struct ETCP_LINK* _lk = _te->etcp_conn->links; _lk; _lk = _lk->next) { \ if (_lk->conn != (_s)) continue; \ if (_lk->link_state == 3) (*_p_lu)++; else (*_p_ld)++; \ } \ } \ } \ } while(0) /* TCP sockets */ { struct ETCP_SOCKET* s = inst->etcp_sockets; while (s) { if (!s->is_tcp) { s = s->next; continue; } char ip_str[INET6_ADDRSTRLEN] = ""; int port = 0; const struct sockaddr_storage* addr = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr; if (addr->ss_family == AF_INET) { struct sockaddr_in* sin = (struct sockaddr_in*)addr; port = ntohs(sin->sin_port); inet_ntop(AF_INET, &sin->sin_addr, ip_str, sizeof(ip_str)); } else if (addr->ss_family == AF_INET6) { struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr; port = ntohs(sin6->sin6_port); inet_ntop(AF_INET6, &sin6->sin6_addr, ip_str, sizeof(ip_str)); } int lu, ld; SOCK_COUNT_LINKS(s, lu, ld); int state = 2; /* bound and listening */ const char* sep = first ? "" : ","; first = 0; const char* pn = s->reality_enabled ? "TCP+REALITY" : "TCP"; size_t needed = snprintf(NULL, 0, "%s{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"%s\",\"st\":%d,\"lu\":%d,\"ld\":%d}", sep, s->sock_id, s->name, ip_str, port, pn, state, lu, ld); 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{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"%s\",\"st\":%d,\"lu\":%d,\"ld\":%d}", sep, s->sock_id, s->name, ip_str, port, pn, state, lu, ld); s = s->next; } } /* UDP sockets */ { struct ETCP_SOCKET* es = inst->etcp_sockets; while (es) { if (es->is_tcp) { es = es->next; continue; } char ip_str[INET6_ADDRSTRLEN] = ""; int port = 0; const struct sockaddr_storage* addr = es->interface_addr.ss_family ? &es->interface_addr : &es->local_addr; if (addr->ss_family == AF_INET) { struct sockaddr_in* sin = (struct sockaddr_in*)addr; port = ntohs(sin->sin_port); inet_ntop(AF_INET, &sin->sin_addr, ip_str, sizeof(ip_str)); } else if (addr->ss_family == AF_INET6) { struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr; port = ntohs(sin6->sin6_port); inet_ntop(AF_INET6, &sin6->sin6_addr, ip_str, sizeof(ip_str)); } int lu, ld; SOCK_COUNT_LINKS(es, lu, ld); int state = 2; /* bound and listening */ const char* sep = first ? "" : ","; first = 0; size_t needed = snprintf(NULL, 0, "%s{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"UDP\",\"st\":%d,\"lu\":%d,\"ld\":%d}", sep, es->sock_id, es->name, ip_str, port, state, lu, ld); 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{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"UDP\",\"st\":%d,\"lu\":%d,\"ld\":%d}", sep, es->sock_id, es->name, ip_str, port, state, lu, ld); es = es->next; } } #undef SOCK_COUNT_LINKS json[pos++] = ']'; json[pos] = '\0'; return json; } void utun_bridge_on_network_change(void) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!inst) return; auto_socket_on_network_change(inst); bridge_log(BLEV_DEBUG, "network change notified to auto_socket"); } static void bridge_set_active_trampoline(void* arg) { int active = (int)(intptr_t)arg; struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!inst) return; if (utun_set_client_activity(inst, active) < 0) { bridge_log(BLEV_ERROR, "client_activity transition failed requested=%s", active ? "ACTIVE" : "STANDBY"); return; } bridge_log(BLEV_DEBUG, "client_activity set to %s", active ? "ACTIVE" : "STANDBY"); } void utun_bridge_set_active(int active) { struct UASYNC* ua = instance_lite_get_uasync(); if (!ua) return; uasync_post(ua, bridge_set_active_trampoline, (void*)(intptr_t)active); } static void bridge_notify_network_active_trampoline(void* arg) { (void)arg; bridge_log(BLEV_DEBUG, "network activity notified to standby"); standby_notify_network_activity(); } void utun_bridge_notify_network_active(void) { struct UASYNC* ua = instance_lite_get_uasync(); if (!ua) return; uasync_post(ua, bridge_notify_network_active_trampoline, NULL); } void utun_bridge_collect_conn_list(void) { instance_lite_collect_conn_list(); } void utun_bridge_collect_conn_metrics(uint64_t peer_node_id) { instance_lite_collect_conn_metrics(peer_node_id); } void utun_bridge_restart(const char* config_text) { bridge_log(BLEV_INFO, "bridge restart: posting to instance"); #ifdef __ANDROID__ { void instance_event_handler(int type, const uint8_t* data, int len); instance_lite_set_event_handler(instance_event_handler); } #endif instance_lite_restart(config_text); /* Wait for instance to be ready before returning to Kotlin. Normal restart (live): is_running() true immediately → no delay. Dead restart (after stop): polls until g_running=1 → blocks until init done. */ for (int i = 0; i < 500 && !instance_lite_is_running(); i++) usleep(10000); if (!instance_lite_is_running()) bridge_log(BLEV_ERROR, "bridge restart: instance not ready after 5s"); } void utun_bridge_update_sockets(const char* config_text) { bridge_log(BLEV_INFO, "bridge update_sockets: posting to instance"); instance_lite_update_sockets(config_text); } void utun_bridge_ping(void) { instance_lite_ping(); } int utun_bridge_is_responsive(void) { return instance_lite_is_responsive(); } /* ────────────────────────────────────────────────────────────────── * Voice recording * ────────────────────────────────────────────────────────────────── */ 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, reply_to, request_id); } int utun_bridge_voice_feed(const int16_t* samples, int count) { return voice_recorder_feed(samples, count); } int utun_bridge_voice_stop(int* out_duration_ms) { return voice_recorder_stop(out_duration_ms); } void utun_bridge_voice_cancel(void) { voice_recorder_cancel(); } int utun_bridge_voice_is_active(void) { return voice_recorder_is_active(); } void utun_bridge_voice_set_preset(int preset) { voice_recorder_set_preset(preset); } int utun_bridge_voice_get_preset(void) { return voice_recorder_get_preset(); } void utun_bridge_voice_set_compressor(int enabled) { voice_recorder_set_compressor_enabled(enabled); } int utun_bridge_voice_get_compressor(void) { return voice_recorder_is_compressor_enabled(); } void utun_bridge_voice_set_compressor_config(int max_gain_db, int lookback_ms, int lookahead_ms, float rise_rate_per_sec, float target_level_db) { voice_recorder_set_compressor_config(max_gain_db, lookback_ms, lookahead_ms, rise_rate_per_sec, target_level_db); } 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, 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) { if (!channel_id || msg_id <= 0) { bridge_log(BLEV_ERROR, "attachment_download: invalid args"); return -1; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) return -1; struct attachment_dl_req* req = u_calloc(1, sizeof(*req)); if (!req) return -1; req->inst = instance_lite_get_instance(); snprintf(req->channel_id, sizeof(req->channel_id), "%s", channel_id); req->msg_id = msg_id; bridge_log(BLEV_INFO, "attachment_download: ch=%s msg_id=%lld", channel_id, (long long)msg_id); uasync_post(ua, chat_core_attachment_download_trampoline, req); return 0; } int utun_bridge_video_send(const char* channel_id, const char* file_path, const char* db_path, 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, 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); } /* ────────────────────────────────────────────────────────────────── * Voice playback (Opus decode) * ────────────────────────────────────────────────────────────────── */ #include "../../../lib/opus_codec.h" struct voice_decode_handle { opus_codec_decoder_t* dec; FILE* file; int sample_rate; int channels; int frame_samples; int total_samples; int pcm_consumed; }; #define OPUS_MAGIC_D 0x5355504F void* utun_bridge_voice_decode_open(const char* file_path, int* out_sample_rate, int* out_channels, int* out_total_samples) { if (!file_path || !out_sample_rate || !out_channels || !out_total_samples) return NULL; FILE* f = fopen(file_path, "rb"); if (!f) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_decode: cannot open %s", file_path); return NULL; } uint32_t magic = 0; uint32_t sr = 0; uint16_t ch = 0, fs = 0; if (fread(&magic, 4, 1, f) != 1 || magic != OPUS_MAGIC_D) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_decode: bad magic"); fclose(f); return NULL; } if (fread(&sr, 4, 1, f) != 1 || fread(&ch, 2, 1, f) != 1 || fread(&fs, 2, 1, f) != 1 || (sr != 8000 && sr != 12000 && sr != 16000 && sr != 24000 && sr != 48000) || (ch != 1 && ch != 2) || fs != sr / 50) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: invalid header rate=%u channels=%u frame=%u", sr, ch, fs); fclose(f); return NULL; } opus_codec_decoder_t* dec = opus_codec_decoder_create((int)sr, (int)ch); if (!dec) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: decoder allocation failed"); fclose(f); return NULL; } struct voice_decode_handle* h = u_calloc(1, sizeof(*h)); if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: handle allocation failed"); opus_codec_decoder_destroy(dec); fclose(f); return NULL; } h->dec = dec; h->file = f; h->sample_rate = (int)sr; h->channels = (int)ch; h->frame_samples = (int)fs; /* count total frames/samples */ long pos = ftell(f); h->total_samples = 0; for (;;) { uint16_t plen = 0; if (fread(&plen, 2, 1, f) != 1) goto invalid_stream; if (!plen) { if (!h->total_samples || fgetc(f) != EOF || ferror(f)) goto invalid_stream; break; } if (plen > 4000 || h->total_samples > INT_MAX - (int)fs * ch || fseek(f, plen, SEEK_CUR)) goto invalid_stream; h->total_samples += (int)fs * (int)ch; } if (fseek(f, pos, SEEK_SET)) goto invalid_stream; *out_sample_rate = h->sample_rate; *out_channels = h->channels; *out_total_samples = h->total_samples; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_decode_open: %s rate=%d ch=%d fs=%d total=%d", file_path, h->sample_rate, h->channels, h->frame_samples, h->total_samples); return h; invalid_stream: DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: truncated/invalid stream path=%s samples=%d", file_path, h->total_samples); utun_bridge_voice_decode_close(h); return NULL; } int utun_bridge_voice_decode_read(void* handle, int16_t* buf, int max_samples) { struct voice_decode_handle* h = (struct voice_decode_handle*)handle; if (!h || !buf || max_samples <= 0) return -1; if (h->pcm_consumed >= h->total_samples) return 0; uint8_t packet[4096]; int written = 0; while (written + h->frame_samples * h->channels <= max_samples) { uint16_t plen; if (fread(&plen, 2, 1, h->file) != 1 || plen == 0 || plen > 4000 || fread(packet, 1, plen, h->file) != plen) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: stream changed after validation"); return -1; } int decoded = opus_codec_decode(h->dec, packet, plen, buf + written, h->frame_samples); if (decoded != h->frame_samples) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: invalid packet result=%d", decoded); return -1; } written += decoded * h->channels; } h->pcm_consumed += written; return written; } void utun_bridge_voice_decode_close(void* handle) { if (!handle) return; struct voice_decode_handle* h = (struct voice_decode_handle*)handle; if (h->dec) opus_codec_decoder_destroy(h->dec); if (h->file) fclose(h->file); u_free(h); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "voice_decode_close"); } /* ────────────────────────────────────────────────────────────────── * P2P аудио-звонок (сигналинг) * ────────────────────────────────────────────────────────────────── */ void utun_bridge_call_start(const char* ch_id, uint64_t peer_node_id, uint64_t call_id) { if (!ch_id || !ch_id[0] || peer_node_id == 0 || call_id == 0) { bridge_log(BLEV_ERROR, "call_start: bad args ch=%s peer=0x%llx id=0x%llx", ch_id ? ch_id : "(null)", (unsigned long long)peer_node_id, (unsigned long long)call_id); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "call_start: uasync not running"); return; } struct call_start_arg* a = (struct call_start_arg*)u_calloc(1, sizeof(*a)); if (!a) { bridge_log(BLEV_ERROR, "call_start: OOM"); return; } a->inst = instance_lite_get_instance(); snprintf(a->ch_id, sizeof(a->ch_id), "%s", ch_id); a->peer_node_id = peer_node_id; a->call_id = call_id; bridge_log(BLEV_INFO, "call_start ch=%s peer=0x%llx id=0x%llx", ch_id, (unsigned long long)peer_node_id, (unsigned long long)call_id); uasync_post(ua, call_start_trampoline, a); } static void bridge_call_id_action(uint64_t call_id, const char* name, void (*trampoline)(void*)) { if (call_id == 0) { bridge_log(BLEV_ERROR, "call_%s: bad call_id=0", name); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "call_%s: uasync not running", name); return; } struct call_id_arg* a = (struct call_id_arg*)u_calloc(1, sizeof(*a)); if (!a) { bridge_log(BLEV_ERROR, "call_%s: OOM", name); return; } a->inst = instance_lite_get_instance(); a->call_id = call_id; bridge_log(BLEV_INFO, "call_%s id=0x%llx", name, (unsigned long long)call_id); uasync_post(ua, trampoline, a); } void utun_bridge_call_accept(uint64_t call_id) { bridge_call_id_action(call_id, "accept", call_accept_trampoline); } void utun_bridge_call_decline(uint64_t call_id) { bridge_call_id_action(call_id, "decline", call_decline_trampoline); } void utun_bridge_call_hangup(uint64_t call_id) { bridge_call_id_action(call_id, "hangup", call_hangup_trampoline); } int utun_bridge_call_is_online(const char* ch_id, uint64_t peer_node_id) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!inst) return 0; return call_is_online(inst, ch_id, peer_node_id); } /* ────────────────────────────────────────────────────────────────── * P2P аудио-звонок (медиа: call_audio.c) * ────────────────────────────────────────────────────────────────── */ int utun_bridge_call_audio_start(uint64_t call_id) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_audio_start: no instance"); return -1; } if (call_audio_init(inst) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_audio_start: call_audio_init failed"); return -1; } return call_audio_start(call_id); } int utun_bridge_call_audio_feed(uint64_t call_id, const int16_t* pcm, int count) { return call_audio_feed_pcm(call_id, pcm, count); } void utun_bridge_call_audio_stop(void) { call_audio_stop(); } int utun_bridge_call_audio_pull(uint64_t call_id, int16_t* out, int max_samples) { return call_audio_pull_pcm(call_id, out, max_samples); } void utun_bridge_call_audio_set_aec(int enabled) { call_audio_set_aec_enabled(enabled); } void utun_bridge_call_audio_set_aec_delay(int frames) { call_audio_set_aec_delay_frames(frames); } /* ────────────────────────────────────────────────────────────────── * Рация (PTT walkie-talkie): подписка + аудио-движок * ────────────────────────────────────────────────────────────────── */ void utun_bridge_radio_set_active(uint64_t group_id, int on) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); struct UASYNC* ua = instance_lite_get_uasync(); if (!inst || !ua) { bridge_log(BLEV_ERROR, "radio_set_active: no instance/ua"); return; } struct radio_set_active_arg* a = (struct radio_set_active_arg*)u_calloc(1, sizeof(*a)); if (!a) return; a->inst = inst; a->group_id = group_id; a->on = on; uasync_post(ua, radio_set_active_trampoline, a); } int utun_bridge_radio_subscribers(uint64_t group_id) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!inst) return 0; return radio_subscribers(inst, group_id); } int utun_bridge_radio_audio_start(uint64_t group_id, int capture_channels) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio_audio_start: no instance"); return -1; } if (radio_audio_init(inst) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio_audio_start: radio_audio_init failed"); return -1; } return radio_audio_start(group_id, capture_channels); } int utun_bridge_radio_audio_feed(uint64_t group_id, const int16_t* pcm, int count) { return radio_audio_feed_pcm(group_id, pcm, count); } void utun_bridge_radio_audio_stop(void) { radio_audio_stop(); } int utun_bridge_radio_audio_pull(uint64_t group_id, int16_t* out, int max_samples) { return radio_audio_pull_pcm(group_id, out, max_samples); } int utun_bridge_radio_auto_mode(void) { return radio_audio_auto_mode(); } void utun_bridge_radio_talk_begin(uint64_t group_id) { radio_audio_talk_begin(group_id); } void utun_bridge_radio_talk_end(uint64_t group_id) { radio_audio_talk_end(group_id); } /* ────────────────────────────────────────────────────────────────── * JNI functions (compiled only for Android via NDK) * ────────────────────────────────────────────────────────────────── */ #ifdef __ANDROID__ static JavaVM* g_jvm = NULL; static jobject g_config_callback = NULL; static jobject g_log_callback = NULL; static jobject g_event_callback = NULL; static jmethodID g_event_mid = NULL; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { (void)reserved; g_jvm = vm; return JNI_VERSION_1_6; } /* ── JNI log callback → LogManager.onNativeLog() ── */ static void jni_on_log(int level, const char* category, const char* message) { if (!g_jvm || !g_log_callback || !category || !message) return; JNIEnv* env = NULL; int attached = 0; jint res = (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6); if (res == JNI_EDETACHED) { if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) != JNI_OK) return; attached = 1; } else if (res != JNI_OK) { return; } jclass cls = (*env)->GetObjectClass(env, g_log_callback); jmethodID mid = (*env)->GetMethodID(env, cls, "onNativeLog", "(ILjava/lang/String;Ljava/lang/String;)V"); if (!mid) { if (attached) (*g_jvm)->DetachCurrentThread(g_jvm); return; } jstring jcat = (*env)->NewStringUTF(env, category); jstring jmsg = (*env)->NewStringUTF(env, message); (*env)->CallVoidMethod(env, g_log_callback, mid, level, jcat, jmsg); (*env)->DeleteLocalRef(env, jmsg); (*env)->DeleteLocalRef(env, jcat); if (attached) (*g_jvm)->DetachCurrentThread(g_jvm); } /* ── JNI event callback (chat_event → Kotlin) ── */ static void jni_on_event(int type, const uint8_t* data, int len) { if (!g_jvm) { bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: g_jvm=NULL"); return; } if (!g_event_callback) { bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: g_event_callback=NULL"); return; } if (!g_event_mid) { bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: g_event_mid=NULL"); return; } JNIEnv* env = NULL; int attached = 0; jint res = (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6); if (res == JNI_EDETACHED) { if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) != JNI_OK) { bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: AttachCurrentThread failed"); return; } attached = 1; } else if (res != JNI_OK) { bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: GetEnv failed res=%d", (int)res); return; } jbyteArray jdata = NULL; if (data && len > 0) { jdata = (*env)->NewByteArray(env, len); if (jdata) (*env)->SetByteArrayRegion(env, jdata, 0, len, (const jbyte*)data); } (*env)->CallVoidMethod(env, g_event_callback, g_event_mid, (jint)type, jdata); if (jdata) (*env)->DeleteLocalRef(env, jdata); if (attached) (*g_jvm)->DetachCurrentThread(g_jvm); } void instance_event_handler(int type, const uint8_t* data, int len) { jni_on_event(type, data, len); } /* ── JNI call-audio: PCM отдаётся через nativeCallAudioPull (см. ниже) ── */ static jstring jni_get_string_callback(JNIEnv* env, const char* key) { if (!g_config_callback) return NULL; jclass cls = (*env)->GetObjectClass(env, g_config_callback); jmethodID mid = (*env)->GetMethodID(env, cls, "getString", "(Ljava/lang/String;)Ljava/lang/String;"); if (!mid) return NULL; jstring jkey = (*env)->NewStringUTF(env, key); jstring result = (jstring)(*env)->CallObjectMethod(env, g_config_callback, mid, jkey); (*env)->DeleteLocalRef(env, jkey); return result; } static const char* jni_cfg_get_string(const char* key) { JNIEnv* env = NULL; if (!g_jvm || (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) return NULL; jstring js = jni_get_string_callback(env, key); if (!js) return NULL; const char* result = (*env)->GetStringUTFChars(env, js, NULL); static char buf[256]; snprintf(buf, sizeof(buf), "%s", result ? result : ""); if (result) (*env)->ReleaseStringUTFChars(env, js, result); (*env)->DeleteLocalRef(env, js); return buf[0] ? buf : NULL; } static int64_t jni_cfg_get_int64(const char* key) { const char* s = jni_cfg_get_string(key); return s ? (int64_t)atoll(s) : 0; } static int jni_cfg_get_int(const char* key) { const char* s = jni_cfg_get_string(key); return s ? atoi(s) : 0; } /* ── nativeStart(configText) — real uTun instance launch ── */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeStart( JNIEnv* env, jobject thiz, jstring configText) { (void)thiz; if (!configText) { bridge_log(BLEV_ERROR, "nativeStart: null config"); return JNI_FALSE; } const char* cfg = (*env)->GetStringUTFChars(env, configText, NULL); if (!cfg) return JNI_FALSE; /* Set up event forwarder (bridge config/log are set by nativeInit) */ instance_lite_set_event_handler(instance_event_handler); int rc = utun_bridge_start(cfg); (*env)->ReleaseStringUTFChars(env, configText, cfg); return (rc >= 0) ? JNI_TRUE : JNI_FALSE; } /* ── nativeStop() ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeStop( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; utun_bridge_destroy(); udp_log_stop(); } /* ── nativeInit (legacy, just saves callbacks) ── */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeInit( JNIEnv* env, jobject thiz, jstring dbPath, jint controlPort, jobject configCallback, jobject logCb) { (void)thiz; if (g_config_callback) (*env)->DeleteGlobalRef(env, g_config_callback); g_config_callback = configCallback ? (*env)->NewGlobalRef(env, configCallback) : NULL; if (g_log_callback) (*env)->DeleteGlobalRef(env, g_log_callback); g_log_callback = logCb ? (*env)->NewGlobalRef(env, logCb) : NULL; const char* db = dbPath ? (*env)->GetStringUTFChars(env, dbPath, NULL) : NULL; if (db) { strncpy(g_db_path, db, sizeof(g_db_path) - 1); g_db_path[sizeof(g_db_path) - 1] = '\0'; } /* Create empty DB file synchronously so ChatRepository (read-only) can open it immediately */ if (g_db_path[0]) { char db_file[512]; snprintf(db_file, sizeof(db_file), "%s/chats.db", g_db_path); sqlite3* js3; if (sqlite3_open_v2(db_file, &js3, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL) == SQLITE_OK) sqlite3_close(js3); } utun_log_fn log_cb = g_log_callback ? jni_on_log : NULL; struct utun_bridge_config cfg = { .get_string = jni_cfg_get_string, .get_int = jni_cfg_get_int, }; bool ok = utun_bridge_init(db, (int)controlPort, log_cb, NULL, &cfg); if (db) (*env)->ReleaseStringUTFChars(env, dbPath, db); return ok ? JNI_TRUE : JNI_FALSE; } /* ── nativeJoinChannel ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeJoinChannel( JNIEnv* env, jobject thiz, jlong channelId, jlong nodeId, jlong joinKey, jbyteArray pubkey, jbyteArray addrsData, jint addrCount) { (void)thiz; if (!pubkey || !addrsData) { bridge_log(BLEV_ERROR, "nativeJoinChannel null args"); return; } jsize pk_len = (*env)->GetArrayLength(env, pubkey); jsize ad_len = (*env)->GetArrayLength(env, addrsData); if (pk_len < 32 || ad_len <= 0) { bridge_log(BLEV_ERROR, "nativeJoinChannel bad sizes pk=%d ad=%d", (int)pk_len, (int)ad_len); return; } uint8_t* pk = (uint8_t*)(*env)->GetByteArrayElements(env, pubkey, NULL); uint8_t* ad = (uint8_t*)(*env)->GetByteArrayElements(env, addrsData, NULL); bridge_log(BLEV_DEBUG, "nativeJoinChannel ch=%lld node=0x%llx pk=%02x%02x... addrs_sz=%d", (long long)channelId, (unsigned long long)nodeId, pk[0], pk[1], (int)ad_len); utun_bridge_join_channel((uint64_t)channelId, (uint64_t)nodeId, pk, ad, (int)addrCount, (int)ad_len, (uint64_t)joinKey); (*env)->ReleaseByteArrayElements(env, addrsData, (jbyte*)ad, JNI_ABORT); (*env)->ReleaseByteArrayElements(env, pubkey, (jbyte*)pk, JNI_ABORT); } /* ── nativeDeriveNodeId ── */ JNIEXPORT jlong JNICALL Java_com_utun_chat_data_NativeLib_nativeDeriveNodeId( JNIEnv* env, jobject thiz, jbyteArray pubkey) { (void)thiz; if (!pubkey) return 0; jsize pk_len = (*env)->GetArrayLength(env, pubkey); if (pk_len < 32) return 0; uint8_t* pk = (uint8_t*)(*env)->GetByteArrayElements(env, pubkey, NULL); uint64_t node_id = sc_derive_node_id_from_pubkey(pk); (*env)->ReleaseByteArrayElements(env, pubkey, (jbyte*)pk, JNI_ABORT); return (jlong)node_id; } /* ── nativeRequestInviteCandidates / nativeRequestInviteLink ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRequestInviteCandidates( JNIEnv* env, jobject thiz, jstring channelId) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); utun_bridge_request_invite_candidates(ch); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRequestInviteLink( JNIEnv* env, jobject thiz, jstring channelId, jlong targetNodeId, jlong requestId) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); utun_bridge_request_invite_link(ch, (uint64_t)targetNodeId, (uint64_t)requestId); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); } /* ── nativeSetEventCallback(eventCb) ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetEventCallback( JNIEnv* env, jobject thiz, jobject callback) { (void)thiz; if (g_event_callback) (*env)->DeleteGlobalRef(env, g_event_callback); g_event_callback = callback ? (*env)->NewGlobalRef(env, callback) : NULL; if (g_event_callback) { jclass cls = (*env)->GetObjectClass(env, g_event_callback); g_event_mid = (*env)->GetMethodID(env, cls, "onEvent", "(I[B)V"); if (!g_event_mid) bridge_log(BLEV_ERROR, "onEvent method not found on callback"); } else { g_event_mid = NULL; } } /* ── Pass-through JNI wrappers (unchanged from before) ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDestroy( JNIEnv* env, jobject thiz) { (void)thiz; utun_bridge_destroy(); udp_log_stop(); if (g_config_callback) { (*env)->DeleteGlobalRef(env, g_config_callback); g_config_callback = NULL; } if (g_log_callback) { (*env)->DeleteGlobalRef(env, g_log_callback); g_log_callback = NULL; } if (g_event_callback) { (*env)->DeleteGlobalRef(env, g_event_callback); g_event_callback = NULL; } } /* Пустой массив означает отсутствие ссылки; иначе принимаем только полный глобальный 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); 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( JNIEnv* env, jobject thiz, jstring name, jstring channelId) { (void)thiz; const char* n = (*env)->GetStringUTFChars(env, name, NULL); const char* c = (*env)->GetStringUTFChars(env, channelId, NULL); utun_bridge_create_channel(n, c); if (c) (*env)->ReleaseStringUTFChars(env, channelId, c); if (n) (*env)->ReleaseStringUTFChars(env, name, n); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeConnectNode( JNIEnv* env, jobject thiz, jstring address, jint port, jstring pubkeyHex) { (void)thiz; const char* a = (*env)->GetStringUTFChars(env, address, NULL); const char* p = (*env)->GetStringUTFChars(env, pubkeyHex, NULL); utun_bridge_connect_node(a, (int)port, p); if (p) (*env)->ReleaseStringUTFChars(env, pubkeyHex, p); if (a) (*env)->ReleaseStringUTFChars(env, address, a); } JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetChannels( JNIEnv* env, jobject thiz) { (void)thiz; char* json = utun_bridge_get_channels_json(); jstring result = (*env)->NewStringUTF(env, json); u_free(json); return result; } JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMessages( 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, (*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; size_t bytes = json ? strlen(json) : 0; jbyteArray data = (*env)->NewByteArray(env, (jsize)bytes); jstring charset = (*env)->NewStringUTF(env, "UTF-8"); jstring result = NULL; if (constructor && data && charset) { (*env)->SetByteArrayRegion(env, data, 0, (jsize)bytes, (const jbyte*)json); if (!(*env)->ExceptionCheck(env)) result = (jstring)(*env)->NewObject(env, strings, constructor, data, charset); } if (!result) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: UTF-8 message result allocation failed"); if (data) (*env)->DeleteLocalRef(env, data); if (charset) (*env)->DeleteLocalRef(env, charset); if (strings) (*env)->DeleteLocalRef(env, strings); u_free(json); (*env)->ReleaseStringUTFChars(env, channelId, ch); 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; const char* ch = sourceChId ? (*env)->GetStringUTFChars(env, sourceChId, NULL) : NULL; const char* nm = peerName ? (*env)->GetStringUTFChars(env, peerName, NULL) : NULL; utun_bridge_dm_start((uint64_t)nodeId, ch, nm); if (nm) (*env)->ReleaseStringUTFChars(env, peerName, nm); if (ch) (*env)->ReleaseStringUTFChars(env, sourceChId, ch); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDmSend( JNIEnv* env, jobject thiz, jstring convId, jstring text) { (void)thiz; const char* c = (*env)->GetStringUTFChars(env, convId, NULL); const char* t = (*env)->GetStringUTFChars(env, text, NULL); utun_bridge_dm_send(c, t); if (t) (*env)->ReleaseStringUTFChars(env, text, t); if (c) (*env)->ReleaseStringUTFChars(env, convId, c); } /* UI получает подтверждение commit через CHAT_EVT_DM_INVITE_RESULT, а не через return JNI. */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeDmInvite( JNIEnv* env, jobject thiz, jlong request_id, jlong peer, jstring source, jlong channel, jstring name, jboolean cancel) { (void)thiz; struct UASYNC* ua = instance_lite_get_uasync(); struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!uasync_is_running(ua) || !inst) { bridge_log(BLEV_ERROR, "invite PM: core unavailable"); return JNI_FALSE; } struct dm_invite_req* req = u_calloc(1, sizeof(*req)); if (!req) { bridge_log(BLEV_ERROR, "invite PM: request allocation failed"); return JNI_FALSE; } const char* src = (*env)->GetStringUTFChars(env, source, NULL); const char* nm = src ? (*env)->GetStringUTFChars(env, name, NULL) : NULL; if (!src || !nm) { if (src) (*env)->ReleaseStringUTFChars(env, source, src); bridge_log(BLEV_ERROR, "invite PM: string conversion failed"); u_free(req); return JNI_FALSE; } req->inst = inst; req->request_id = (uint64_t)request_id; req->peer_node_id = (uint64_t)peer; req->channel_id = (uint64_t)channel; req->cancel = cancel ? 1 : 0; snprintf(req->source_ch_id, sizeof(req->source_ch_id), "%s", src); snprintf(req->peer_name, sizeof(req->peer_name), "%s", nm); (*env)->ReleaseStringUTFChars(env, source, src); (*env)->ReleaseStringUTFChars(env, name, nm); uasync_post(ua, dm_invite_trampoline, req); return JNI_TRUE; } /* Чтение членства для выбора групп и открытия уже присоединённого канала. */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeIsChannelMember( JNIEnv* env, jobject thiz, jstring channel, jlong node) { (void)thiz; struct UTUN_INSTANCE* inst = instance_lite_get_instance(); const char* id = (*env)->GetStringUTFChars(env, channel, NULL); if (!id) { bridge_log(BLEV_ERROR, "channel membership: string conversion failed"); return JNI_FALSE; } int member = inst && topo_node_sqlite_member_in_channel(inst->topo_sqlite_db, id, (uint64_t)node); (*env)->ReleaseStringUTFChars(env, channel, id); return member ? JNI_TRUE : JNI_FALSE; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDmRequestConversations( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; utun_bridge_dm_request_conversations(); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDmRequestCallRoute( JNIEnv* env, jobject thiz, jstring convId) { (void)thiz; struct UASYNC* ua = instance_lite_get_uasync(); if (!uasync_is_running(ua)) { bridge_log(BLEV_WARN, "PM call route: core stopped"); return; } const char* conv = (*env)->GetStringUTFChars(env, convId, NULL); if (!conv) return; struct dm_messages_req* req = u_calloc(1, sizeof(*req)); if (req) { req->inst = instance_lite_get_instance(); snprintf(req->conv_id, sizeof(req->conv_id), "%s", conv); uasync_post(ua, dm_call_route_trampoline, req); } else bridge_log(BLEV_ERROR, "PM call route: allocation failed"); (*env)->ReleaseStringUTFChars(env, convId, conv); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDmRequestMessages( JNIEnv* env, jobject thiz, jstring convId) { (void)thiz; const char* c = (*env)->GetStringUTFChars(env, convId, NULL); utun_bridge_dm_request_messages(c); if (c) (*env)->ReleaseStringUTFChars(env, convId, c); } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeDmSendFile( JNIEnv* env, jobject thiz, jstring convId, jstring path, jstring name) { (void)thiz; const char* c = (*env)->GetStringUTFChars(env, convId, NULL); const char* p = (*env)->GetStringUTFChars(env, path, NULL); const char* n = (*env)->GetStringUTFChars(env, name, NULL); int rc = utun_bridge_dm_send_file(c, p, n); if (n) (*env)->ReleaseStringUTFChars(env, name, n); if (p) (*env)->ReleaseStringUTFChars(env, path, p); if (c) (*env)->ReleaseStringUTFChars(env, convId, c); return rc == 0 ? JNI_TRUE : JNI_FALSE; } JNIEXPORT jlong JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMyNodeId( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; return (jlong)utun_bridge_get_my_node_id(); } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRestart( JNIEnv* env, jobject thiz, jstring configText) { (void)thiz; if (!configText) return JNI_FALSE; const char* cfg = (*env)->GetStringUTFChars(env, configText, NULL); if (!cfg) return JNI_FALSE; utun_bridge_restart(cfg); (*env)->ReleaseStringUTFChars(env, configText, cfg); return JNI_TRUE; } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeUpdateSockets( JNIEnv* env, jobject thiz, jstring configText) { (void)thiz; if (!configText) return JNI_FALSE; const char* cfg = (*env)->GetStringUTFChars(env, configText, NULL); if (!cfg) return JNI_FALSE; utun_bridge_update_sockets(cfg); (*env)->ReleaseStringUTFChars(env, configText, cfg); return JNI_TRUE; } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeIsResponsive( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; return utun_bridge_is_responsive() ? JNI_TRUE : JNI_FALSE; } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeIsRunning( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; return instance_lite_is_running() ? JNI_TRUE : JNI_FALSE; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativePing( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; utun_bridge_ping(); } /* GUI запрашивает готовый снимок асинхронно, CPU-часы читает только поток ядра. */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRequestCpuLoad(JNIEnv* env, jobject thiz) { (void)env; (void)thiz; struct UASYNC* ua = instance_lite_get_uasync(); struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!ua || !inst || !instance_lite_is_running()) return JNI_FALSE; struct posted_task* task = u_calloc(1, sizeof(*task)); if (!task) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "CPU load request allocation failed"); return JNI_FALSE; } task->callback = chat_core_collect_cpu_load_trampoline; task->arg = inst; uasync_post_reserved(ua, task); return JNI_TRUE; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeConnectChannel( JNIEnv* env, jobject thiz, jstring channelId) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); utun_bridge_connect_channel(ch); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeConnectMemberNode( JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); utun_bridge_connect_member_node(ch, (uint64_t)nodeId); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetDebugLevel( JNIEnv* env, jobject thiz, jstring category, jstring level) { (void)thiz; const char* c = (*env)->GetStringUTFChars(env, category, NULL); const char* l = (*env)->GetStringUTFChars(env, level, NULL); utun_bridge_set_debug_level(c, l); if (l) (*env)->ReleaseStringUTFChars(env, level, l); if (c) (*env)->ReleaseStringUTFChars(env, category, c); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetChatSetting( JNIEnv* env, jobject thiz, jstring name, jstring value) { (void)thiz; const char* n = (*env)->GetStringUTFChars(env, name, NULL); const char* v = (*env)->GetStringUTFChars(env, value, NULL); struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (inst) chat_core_set_setting(inst, n, v); if (v) (*env)->ReleaseStringUTFChars(env, value, v); if (n) (*env)->ReleaseStringUTFChars(env, name, n); } JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeGetChatSettingInt( JNIEnv* env, jobject thiz, jstring name, jint def) { (void)thiz; const char* n = (*env)->GetStringUTFChars(env, name, NULL); struct UTUN_INSTANCE* inst = instance_lite_get_instance(); int val = chat_setting_get_int(inst, n, (int)def); if (n) (*env)->ReleaseStringUTFChars(env, name, n); return (jint)val; } JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeGetSoundOnMessage( JNIEnv* env, jobject thiz, jstring chId) { (void)thiz; const char* c = (*env)->GetStringUTFChars(env, chId, NULL); int on = utun_bridge_get_sound_on_message(c); if (c) (*env)->ReleaseStringUTFChars(env, chId, c); return (jint)on; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetSoundOnMessage( JNIEnv* env, jobject thiz, jstring chId, jboolean on) { (void)thiz; const char* c = (*env)->GetStringUTFChars(env, chId, NULL); utun_bridge_set_sound_on_message(c, on ? 1 : 0); if (c) (*env)->ReleaseStringUTFChars(env, chId, c); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSubscribeLogs( JNIEnv* env, jobject thiz, jboolean enabled) { (void)thiz; (void)env; g_on_log = (enabled && g_log_callback) ? jni_on_log : NULL; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetUdpLogTarget( JNIEnv* env, jobject thiz, jstring ip, jint port) { (void)thiz; const char* ip_str = (*env)->GetStringUTFChars(env, ip, NULL); udp_log_set_target(ip_str, (int)port); (*env)->ReleaseStringUTFChars(env, ip, ip_str); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRegenerateKeys( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; instance_lite_regenerate_keys(); } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeClearDatabase( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; if (!g_db_path[0]) { bridge_log(BLEV_ERROR, "clearDatabase: no db path"); return JNI_FALSE; } char db_file[512], wal_file[512], shm_file[512]; snprintf(db_file, sizeof(db_file), "%s/chats.db", g_db_path); snprintf(wal_file, sizeof(wal_file), "%s/chats.db-wal", g_db_path); snprintf(shm_file, sizeof(shm_file), "%s/chats.db-shm", g_db_path); unlink(db_file); unlink(wal_file); unlink(shm_file); bridge_log(BLEV_INFO, "clearDatabase: deleted DB files at %s", g_db_path); return JNI_TRUE; } /* ── Voice recording JNI ── */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceStart( 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, reply, (uint64_t)requestId); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); return (rc == 0) ? JNI_TRUE : JNI_FALSE; } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceFeed( JNIEnv* env, jobject thiz, jshortArray samples) { (void)env; (void)thiz; if (!samples) return JNI_FALSE; jsize len = (*env)->GetArrayLength(env, samples); jshort* data = (*env)->GetShortArrayElements(env, samples, NULL); int rc = utun_bridge_voice_feed((const int16_t*)data, (int)len); (*env)->ReleaseShortArrayElements(env, samples, data, JNI_ABORT); return (rc == 0) ? JNI_TRUE : JNI_FALSE; } JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceStop( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; int duration_ms = 0; return utun_bridge_voice_stop(&duration_ms) == 0 ? (jint)duration_ms : -1; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceCancel( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; utun_bridge_voice_cancel(); } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceIsActive( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; return utun_bridge_voice_is_active() ? JNI_TRUE : JNI_FALSE; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceSetPreset( JNIEnv* env, jobject thiz, jint preset) { (void)env; (void)thiz; utun_bridge_voice_set_preset((int)preset); } JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceGetPreset( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; return (jint)utun_bridge_voice_get_preset(); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceSetCompressor( JNIEnv* env, jobject thiz, jboolean enabled) { (void)env; (void)thiz; utun_bridge_voice_set_compressor(enabled ? 1 : 0); } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceGetCompressor( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; return utun_bridge_voice_get_compressor() ? JNI_TRUE : JNI_FALSE; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceSetCompressorConfig( JNIEnv* env, jobject thiz, jint maxGainDb, jint lookbackMs, jint lookaheadMs, jfloat riseRate, jfloat targetLevelDb) { (void)env; (void)thiz; utun_bridge_voice_set_compressor_config((int)maxGainDb, (int)lookbackMs, (int)lookaheadMs, (float)riseRate, (float)targetLevelDb); } JNIEXPORT jfloat JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceGetPeakLevel( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; return (jfloat)utun_bridge_voice_get_peak_level(); } /* ── Attachment JNI ── */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeAttachmentSend( 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, 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; } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeAttachmentDownload( JNIEnv* env, jobject thiz, jstring channelId, jlong msgId) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); int rc = utun_bridge_attachment_download(ch, (int64_t)msgId); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); return (rc == 0) ? JNI_TRUE : JNI_FALSE; } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVideoSend( JNIEnv* env, jobject thiz, jstring channelId, jstring filePath, 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, 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; } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativePhotoSend( JNIEnv* env, jobject thiz, jstring channelId, jstring filePath, 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, 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; } /* ── Voice playback JNI ── */ JNIEXPORT jlong JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceDecodeOpen( JNIEnv* env, jobject thiz, jstring filePath, jintArray outInfo) { (void)thiz; if (!filePath || !outInfo) return 0; const char* fp = (*env)->GetStringUTFChars(env, filePath, NULL); int sr = 0, ch = 0, total = 0; void* h = utun_bridge_voice_decode_open(fp, &sr, &ch, &total); if (h) { jint info[3] = {sr, ch, total}; (*env)->SetIntArrayRegion(env, outInfo, 0, 3, info); } if (fp) (*env)->ReleaseStringUTFChars(env, filePath, fp); return (jlong)(intptr_t)h; } JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceDecodeRead( JNIEnv* env, jobject thiz, jlong handle, jshortArray buf) { (void)thiz; if (handle == 0 || !buf) return -1; jsize max = (*env)->GetArrayLength(env, buf); jshort* data = (*env)->GetShortArrayElements(env, buf, NULL); int read = utun_bridge_voice_decode_read((void*)(intptr_t)handle, (int16_t*)data, (int)max); (*env)->ReleaseShortArrayElements(env, buf, data, 0); return (jint)read; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceDecodeClose( JNIEnv* env, jobject thiz, jlong handle) { (void)env; (void)thiz; utun_bridge_voice_decode_close((void*)(intptr_t)handle); } /* ── Channel members JNI ── */ /* Живой снимок группы читается в uasync; результат приходит как MEMBER_LIST. */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRequestMemberList( JNIEnv* env, jobject thiz, jstring channelId) { (void)thiz; struct UTUN_INSTANCE* inst = instance_lite_get_instance(); struct UASYNC* ua = instance_lite_get_uasync(); if (!inst || !ua || !channelId) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "request_member_list: missing instance/uasync/channel"); return JNI_FALSE; } const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); if (!ch) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "request_member_list: cannot read channel"); return JNI_FALSE; } struct chat_member_list_req* req = u_calloc(1, sizeof(*req)); if (!req || !ch[0] || strlen(ch) >= sizeof(req->ch_id)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "request_member_list: allocation/invalid channel ch=%s", ch); u_free(req); (*env)->ReleaseStringUTFChars(env, channelId, ch); return JNI_FALSE; } req->inst = inst; memcpy(req->ch_id, ch, strlen(ch) + 1); DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "request_member_list: queued ch=%s", ch); (*env)->ReleaseStringUTFChars(env, channelId, ch); uasync_post(ua, chat_core_request_member_list_trampoline, req); return JNI_TRUE; } JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetChannelMembers( JNIEnv* env, jobject thiz, jstring channelId) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); char* json = utun_bridge_get_channel_members_json(ch); jstring result = (*env)->NewStringUTF(env, json); u_free(json); (*env)->ReleaseStringUTFChars(env, channelId, ch); return result; } JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMemberDetail( JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); char* json = utun_bridge_get_member_detail_json(ch, (uint64_t)nodeId); jstring result = (*env)->NewStringUTF(env, json); u_free(json); (*env)->ReleaseStringUTFChars(env, channelId, ch); return result; } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeToggleMemberRadioMute( JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId) { (void)thiz; if (!channelId) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: null Android mute channel"); return JNI_FALSE; } const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); if (!ch) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: Android mute channel conversion failed"); return JNI_FALSE; } int rc = utun_bridge_toggle_member_radio_mute(ch, (uint64_t)nodeId); (*env)->ReleaseStringUTFChars(env, channelId, ch); return rc == 0 ? JNI_TRUE : JNI_FALSE; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetMemberNick( JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId, jstring nick) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); const char* nk = nick ? (*env)->GetStringUTFChars(env, nick, NULL) : NULL; utun_bridge_set_member_nick(ch, (uint64_t)nodeId, nk); if (nk) (*env)->ReleaseStringUTFChars(env, nick, nk); (*env)->ReleaseStringUTFChars(env, channelId, ch); } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeIsChannelOwner( JNIEnv* env, jobject thiz, jstring channelId) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); int owner = utun_bridge_is_channel_owner(ch); (*env)->ReleaseStringUTFChars(env, channelId, ch); return (jboolean)owner; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeTransferAdmin( JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); utun_bridge_transfer_admin(ch, (uint64_t)nodeId); (*env)->ReleaseStringUTFChars(env, channelId, ch); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDeleteChannel( JNIEnv* env, jobject thiz, jstring channelId) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); utun_bridge_delete_channel(ch); (*env)->ReleaseStringUTFChars(env, channelId, ch); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetMemberFlags( JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId, jboolean admin, jboolean supernode, jboolean moder, jboolean storage, jboolean verified, jboolean deleted) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); utun_bridge_set_member_flags(ch, (uint64_t)nodeId, admin, supernode, moder, storage, verified, deleted); (*env)->ReleaseStringUTFChars(env, channelId, ch); } JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMemberLinks( JNIEnv* env, jobject thiz, jlong nodeId) { (void)thiz; char* json = utun_bridge_get_member_links_json((uint64_t)nodeId); jstring result = (*env)->NewStringUTF(env, json); u_free(json); return result; } /* ── Local sockets JSON ── */ JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetLocalSockets( JNIEnv* env, jobject thiz) { (void)thiz; char* json = utun_bridge_get_local_sockets_json(); jstring result = (*env)->NewStringUTF(env, json ? json : "[]"); u_free(json); return result; } /* ── Network change notification ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeOnNetworkChanged( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; utun_bridge_on_network_change(); } /* ── App foreground/background (Android lifecycle) → client_activity ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetActive( JNIEnv* env, jobject thiz, jboolean active) { (void)env; (void)thiz; utun_bridge_set_active(active ? 1 : 0); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeNotifyNetworkActive( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; utun_bridge_notify_network_active(); } /* ── Connection monitor ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCollectConnList( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; utun_bridge_collect_conn_list(); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCollectConnMetrics( JNIEnv* env, jobject thiz, jlong peerNodeId) { (void)env; (void)thiz; utun_bridge_collect_conn_metrics((uint64_t)peerNodeId); } /* ── P2P аудио-звонок (сигналинг) ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallStart( JNIEnv* env, jobject thiz, jstring channelId, jlong peerNodeId, jlong callId) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); utun_bridge_call_start(ch, (uint64_t)peerNodeId, (uint64_t)callId); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAccept( JNIEnv* env, jobject thiz, jlong callId) { (void)env; (void)thiz; utun_bridge_call_accept((uint64_t)callId); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallDecline( JNIEnv* env, jobject thiz, jlong callId) { (void)env; (void)thiz; utun_bridge_call_decline((uint64_t)callId); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallHangup( JNIEnv* env, jobject thiz, jlong callId) { (void)env; (void)thiz; utun_bridge_call_hangup((uint64_t)callId); } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeCallIsOnline( JNIEnv* env, jobject thiz, jstring channelId, jlong peerNodeId) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); int online = utun_bridge_call_is_online(ch, (uint64_t)peerNodeId); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); return online ? JNI_TRUE : JNI_FALSE; } /* ── P2P аудио-звонок (медиа) ── */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioStart( JNIEnv* env, jobject thiz, jlong callId) { (void)env; (void)thiz; return (utun_bridge_call_audio_start((uint64_t)callId) == 0) ? JNI_TRUE : JNI_FALSE; } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioFeed( JNIEnv* env, jobject thiz, jlong callId, jshortArray samples) { (void)env; (void)thiz; if (!samples) return JNI_FALSE; jsize len = (*env)->GetArrayLength(env, samples); jshort* data = (*env)->GetShortArrayElements(env, samples, NULL); int rc = utun_bridge_call_audio_feed((uint64_t)callId, (const int16_t*)data, (int)len); (*env)->ReleaseShortArrayElements(env, samples, data, JNI_ABORT); return (rc == 0) ? JNI_TRUE : JNI_FALSE; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioStop( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; utun_bridge_call_audio_stop(); } /* ── nativeCallAudioPull(callId, out) → возвращает число сэмплов ── * out — предвыделенный ShortArray (960), заполняется из аудио-потока Kotlin. */ JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioPull( JNIEnv* env, jobject thiz, jlong callId, jshortArray out) { (void)thiz; if (!out) return 0; jsize len = (*env)->GetArrayLength(env, out); jshort* data = (*env)->GetShortArrayElements(env, out, NULL); if (!data) return 0; int n = utun_bridge_call_audio_pull((uint64_t)callId, (int16_t*)data, (int)len); (*env)->ReleaseShortArrayElements(env, out, data, 0); return (jint)n; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioSetAec( JNIEnv* env, jobject thiz, jboolean enabled) { (void)env; (void)thiz; utun_bridge_call_audio_set_aec(enabled ? 1 : 0); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioSetAecDelay( JNIEnv* env, jobject thiz, jint frames) { (void)env; (void)thiz; utun_bridge_call_audio_set_aec_delay((int)frames); } /* ── Рация (PTT walkie-talkie) ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioSetActive( JNIEnv* env, jobject thiz, jlong groupId, jboolean on) { (void)env; (void)thiz; utun_bridge_radio_set_active((uint64_t)groupId, on ? 1 : 0); } JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioSubscribers( JNIEnv* env, jobject thiz, jlong groupId) { (void)env; (void)thiz; return (jint)utun_bridge_radio_subscribers((uint64_t)groupId); } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioAudioStart( JNIEnv* env, jobject thiz, jlong groupId, jint channels) { (void)env; (void)thiz; return (utun_bridge_radio_audio_start((uint64_t)groupId, (int)channels) == 0) ? JNI_TRUE : JNI_FALSE; } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioCaptureStart( JNIEnv* env, jobject thiz, jlong groupId, jint channels, jint mode) { (void)env; (void)thiz; return radio_audio_capture_start((uint64_t)groupId, (int)channels, (int)mode) == 0 ? JNI_TRUE : JNI_FALSE; } /* Снимок настроек применяется целиком в потоке ядра; callback разрешает новый capture. */ struct radio_config_request { struct posted_task task; struct UTUN_INSTANCE* inst; jlong request_id; jint values[7]; }; static void radio_config_applied(jlong request_id, int success) { JNIEnv* env = NULL; int attached = 0; if (!g_jvm || !g_config_callback) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config callback unavailable request=%lld", (long long)request_id); return; } jint rc = (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6); if (rc == JNI_EDETACHED) { if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) != JNI_OK) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config: attach JVM failed"); return; } attached = 1; } else if (rc != JNI_OK) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config: GetEnv failed rc=%d", rc); return; } jclass cls = (*env)->GetObjectClass(env, g_config_callback); jmethodID method = cls ? (*env)->GetMethodID(env, cls, "onRadioSettingsApplied", "(JZ)V") : NULL; if (method) (*env)->CallVoidMethod(env, g_config_callback, method, request_id, success ? JNI_TRUE : JNI_FALSE); if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config callback failed request=%lld", (long long)request_id); } if (cls) (*env)->DeleteLocalRef(env, cls); if (attached) (*g_jvm)->DetachCurrentThread(g_jvm); } static void radio_config_apply(void* arg) { struct radio_config_request* req = arg; static const char* keys[] = {"radio_auto_enabled", "radio_auto_mode", "radio_level_on_dbfs", "radio_level_off_dbfs", "radio_auto_hangover_ms", "radio_vad_threshold", "radio_vad_compressor_max_gain_db"}; int success = req->inst == instance_lite_get_instance(); if (success) { struct chat_setting_state next = req->inst->chat_settings; for (int i = 0; i < 7; ++i) { char value[16]; snprintf(value, sizeof(value), "%d", req->values[i]); if (chat_setting_set_state(&next, keys[i], value) != 0) success = 0; } if (req->values[3] >= req->values[2]) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config: invalid thresholds on=%d off=%d", req->values[2], req->values[3]); success = 0; } if (success) req->inst->chat_settings = next; } else DEBUG_WARN(DEBUG_CATEGORY_RADIO, "Android radio config: stale instance request=%lld", (long long)req->request_id); DEBUG_INFO(DEBUG_CATEGORY_RADIO, "Android radio config request=%lld success=%d enabled=%d mode=%d on=%d off=%d hangover=%dms", (long long)req->request_id, success, req->values[0], req->values[1], req->values[2], req->values[3], req->values[4]); radio_config_applied(req->request_id, success); } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioConfigure( JNIEnv* env, jobject thiz, jlong request_id, jintArray settings) { (void)thiz; struct UTUN_INSTANCE* inst = instance_lite_get_instance(); struct UASYNC* ua = instance_lite_get_uasync(); if (!inst || !uasync_is_running(ua) || !settings || (*env)->GetArrayLength(env, settings) != 7) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config: core unavailable or invalid snapshot request=%lld", (long long)request_id); return JNI_FALSE; } struct radio_config_request* req = u_calloc(1, sizeof(*req)); if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config allocation failed"); return JNI_FALSE; } (*env)->GetIntArrayRegion(env, settings, 0, 7, req->values); if ((*env)->ExceptionCheck(env)) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config: cannot copy snapshot"); u_free(req); return JNI_FALSE; } req->inst = inst; req->request_id = request_id; req->task.callback = radio_config_apply; req->task.arg = req; uasync_post_reserved(ua, &req->task); // Очередь освобождает снимок, в том числе при shutdown. return JNI_TRUE; } JNIEXPORT jfloatArray JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioInputStatus(JNIEnv* env, jobject thiz) { (void)thiz; struct radio_audio_input_status status = radio_audio_get_input_status(); jfloat values[] = {status.level_dbfs, (float)status.valid, (float)status.blocked, status.vad_probability, (float)status.vad_valid, status.vad_gain_db, (float)status.vad_compressor_enabled}; jfloatArray result = (*env)->NewFloatArray(env, 7); if (!result) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio input status allocation failed"); return NULL; } (*env)->SetFloatArrayRegion(env, result, 0, 7, values); return result; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioCaptureStop(JNIEnv* env, jobject thiz) { (void)env; (void)thiz; radio_audio_capture_stop(); } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioAudioFeed( JNIEnv* env, jobject thiz, jlong groupId, jshortArray samples) { (void)thiz; if (!samples) return JNI_FALSE; jsize len = (*env)->GetArrayLength(env, samples); jshort* data = (*env)->GetShortArrayElements(env, samples, NULL); int rc = utun_bridge_radio_audio_feed((uint64_t)groupId, (const int16_t*)data, (int)len); (*env)->ReleaseShortArrayElements(env, samples, data, JNI_ABORT); return (rc == 0) ? JNI_TRUE : JNI_FALSE; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioAudioStop( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; utun_bridge_radio_audio_stop(); } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioTransmitting(JNIEnv* env, jobject thiz) { (void)env; (void)thiz; return radio_audio_transmitting() ? JNI_TRUE : JNI_FALSE; } JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioAutoMode( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; return (jint)utun_bridge_radio_auto_mode(); } JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioAudioPull( JNIEnv* env, jobject thiz, jlong groupId, jshortArray out, jlong playedFrames) { (void)thiz; if (!out) return 0; jsize len = (*env)->GetArrayLength(env, out); jshort* data = (*env)->GetShortArrayElements(env, out, NULL); if (!data) return 0; radio_audio_playback_progress((uint64_t)groupId, (uint64_t)playedFrames, 0); int n = utun_bridge_radio_audio_pull((uint64_t)groupId, (int16_t*)data, (int)len); (*env)->ReleaseShortArrayElements(env, out, data, 0); return (jint)n; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioTalkBegin( JNIEnv* env, jobject thiz, jlong groupId) { (void)env; (void)thiz; utun_bridge_radio_talk_begin((uint64_t)groupId); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioTalkEnd( JNIEnv* env, jobject thiz, jlong groupId) { (void)env; (void)thiz; utun_bridge_radio_talk_end((uint64_t)groupId); } /* Снимок читается в потоке ядра; локальное JNI-событие 53 не меняет сетевой протокол. * id/group/version:8LE, hash:32, can_edit/failed:1, size:2LE, data:size. */ static void android_avatar_snapshot(void* arg) { struct group_avatar_gui_request* req = arg; struct group_avatar_record record; memset(&record, 0, sizeof(record)); int failed = group_avatar_get(req->inst, req->group, &record) < 0; char channel[32]; uint8_t key[32]; snprintf(channel, sizeof(channel), "%llu", (unsigned long long)req->group); int can_edit = topo_node_sqlite_channel_get_priv(chat_core_get_db(req->inst), channel, key) == 0; memset(key, 0, sizeof(key)); if (failed) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "Android avatar: snapshot failed group=%s id=%llu", channel, (unsigned long long)req->id); size_t size = failed ? 0 : record.size; uint8_t* event = u_calloc(1, 60 + size); if (!event) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "Android avatar: snapshot allocation failed id=%llu", (unsigned long long)req->id); u_free(req); return; } mf_put64(event, req->id); mf_put64(event + 8, req->group); mf_put64(event + 16, record.version); memcpy(event + 24, record.hash, 32); event[56] = can_edit; event[57] = failed; event[58] = size & 255; event[59] = size >> 8; if (size) memcpy(event + 60, record.data, size); DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "Android avatar: snapshot group=%s id=%llu version=%llu size=%zu edit=%d failed=%d", channel, (unsigned long long)req->id, (unsigned long long)record.version, size, can_edit, failed); jni_on_event(53, event, (int)(60 + size)); u_free(event); u_free(req); } /* Только операции аватарок; файловые пути ограничены каталогом avatars. Владение req переходит uasync. */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeAvatarOperation( JNIEnv* env, jobject thiz, jint op, jlong id, jlong group, jlong server, jlong owner, jstring remote, jstring local, jbyteArray data) { (void)thiz; struct UTUN_INSTANCE* inst = instance_lite_get_instance(); struct UASYNC* ua = instance_lite_get_uasync(); if (!inst || !ua || !uasync_is_running(ua) || !id || (!group && op != 7) || op < 0 || op > 8) { DEBUG_WARN(DEBUG_CATEGORY_CHAT, "Android avatar: unavailable/invalid operation=%d id=%lld group=%llx", op, (long long)id, (unsigned long long)group); return JNI_FALSE; } const char* path = remote ? (*env)->GetStringUTFChars(env, remote, NULL) : NULL; const char* file = local ? (*env)->GetStringUTFChars(env, local, NULL) : NULL; void* req = NULL; uasync_post_callback_t callback = NULL; if ((remote && !path) || (local && !file)) goto invalid; if (((op >= 1 && op <= 4) || op == 8) && (!server || !owner || !path || (op == 3 || op == 8 ? strcmp(path, "avatars") != 0 : strncmp(path, "avatars/", 8) != 0 || !mf_avatar_file_valid(path + 8)))) goto invalid; if (op == 0 || op == 6) { struct group_avatar_gui_request* r = u_calloc(1, sizeof(*r)); req = r; if (!r) goto invalid; r->inst = inst; r->id = id; r->group = group; if (op == 6) { jsize size = data ? (*env)->GetArrayLength(env, data) : 0; if (size > GROUP_AVATAR_MAX) goto invalid; r->size = size; if (size) (*env)->GetByteArrayRegion(env, data, 0, size, (jbyte*)r->data); if ((*env)->ExceptionCheck(env)) goto invalid; } callback = op == 0 ? android_avatar_snapshot : group_avatar_gui_set; } else if (op == 1 || op == 2) { if (!file || !file[0] || strlen(file) > MF_PATH_MAX) goto invalid; struct mf_transfer_request* r = u_calloc(1, sizeof(*r)); req = r; if (!r) goto invalid; r->inst = inst; r->id = id; r->group = group; r->server = server; r->owner = owner; uint8_t limit[8]; if (!data || (*env)->GetArrayLength(env, data) != 8) goto invalid; (*env)->GetByteArrayRegion(env, data, 0, 8, (jbyte*)limit); if ((*env)->ExceptionCheck(env)) goto invalid; r->upload = op == 2; r->max_bytes = mf_get64(limit); if (!r->max_bytes || r->max_bytes > mf_avatar_file_limit(path + 8)) goto invalid; memcpy(r->remote, path, strlen(path) + 1); memcpy(r->local, file, strlen(file) + 1); callback = member_files_gui_transfer; } else if (op == 3 || op == 4 || op == 8) { struct mf_gui_request* r = u_calloc(1, sizeof(*r)); req = r; if (!r) goto invalid; r->inst = inst; r->group = group; r->server = server; r->message.kind = MF_REQUEST; r->message.command = op == 3 ? MF_MKDIR : op == 8 ? MF_LIST : MF_STAT; r->message.request_id = id; r->message.owner = owner; if (op == 8) { uint8_t page[24]; if (!data || (*env)->GetArrayLength(env, data) != 24) goto invalid; (*env)->GetByteArrayRegion(env, data, 0, 24, (jbyte*)page); if ((*env)->ExceptionCheck(env)) goto invalid; r->message.cursor = mf_get64(page); memcpy(r->message.version, page + 8, 16); } memcpy(r->message.path, path, strlen(path) + 1); callback = member_files_gui_request; } else if (op == 5) { if (server && (!path || !mf_avatar_manifest_valid(path))) goto invalid; struct chat_avatar_req* r = u_calloc(1, sizeof(*r)); req = r; if (!r) goto invalid; r->inst = inst; r->id = id; r->group = group; r->server = server; if (server) memcpy(r->file, path, strlen(path) + 1); callback = chat_core_set_my_avatar_trampoline; } else { struct mf_cancel_request* r = u_calloc(1, sizeof(*r)); req = r; if (!r) goto invalid; r->inst = inst; r->id = id; callback = member_files_gui_cancel; } if (path) (*env)->ReleaseStringUTFChars(env, remote, path); if (file) (*env)->ReleaseStringUTFChars(env, local, file); DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "Android avatar: queued operation=%d id=%lld group=%016llx server=%016llx owner=%016llx", op, (long long)id, (unsigned long long)group, (unsigned long long)server, (unsigned long long)owner); uasync_post(ua, callback, req); return JNI_TRUE; invalid: DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "Android avatar: invalid arguments/allocation operation=%d id=%lld group=%016llx", op, (long long)id, (unsigned long long)group); if (path) (*env)->ReleaseStringUTFChars(env, remote, path); if (file) (*env)->ReleaseStringUTFChars(env, local, file); u_free(req); return JNI_FALSE; } /* Общие файловые команды. Wire уже UTF-8, поэтому JNI modified UTF-8 не портит имена с emoji. */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeFileRequest( JNIEnv* env, jobject thiz, jlong group, jlong server, jbyteArray wire) { (void)thiz; struct UTUN_INSTANCE* inst = instance_lite_get_instance(); struct UASYNC* ua = instance_lite_get_uasync(); jsize size = wire ? (*env)->GetArrayLength(env, wire) : 0; struct mf_gui_request* q = NULL; if (!inst || !ua || !uasync_is_running(ua) || !group || !server || size < MF_WIRE_HEADER || size > MF_WIRE_MAX) goto invalid; uint8_t bytes[MF_WIRE_MAX]; (*env)->GetByteArrayRegion(env, wire, 0, size, (jbyte*)bytes); if ((*env)->ExceptionCheck(env)) goto invalid; q = u_calloc(1, sizeof(*q)); if (!q || mf_decode(bytes, size, &q->message)) goto invalid; struct mf_message* m = &q->message; int read = m->command == MF_LIST || m->command == MF_STAT || m->command == MF_QUOTA; int write = m->command == MF_MKDIR || m->command == MF_RENAME || m->command == MF_REMOVE_PATH; if (m->kind != MF_REQUEST || (!read && !write) || m->data_len || m->offset || m->size || m->more || !mf_path_valid(m->path, read) || (!m->owner && (m->command != MF_LIST || m->path[0])) || (m->command == MF_RENAME ? !mf_path_valid(m->target, 0) : m->target[0] != 0)) goto invalid; if (write && (m->owner != inst->node_id || (!strncasecmp(m->path, "avatars", 7) && (!m->path[7] || m->path[7] == '/')))) goto invalid; if (m->command == MF_RENAME && !strncasecmp(m->target, "avatars", 7) && (!m->target[7] || m->target[7] == '/')) goto invalid; q->inst = inst; q->group = (uint64_t)group; q->server = (uint64_t)server; DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "Android files: request id=%llu cmd=%s group=%llu server=%llu owner=%llu path=%s", (unsigned long long)m->request_id, mf_command_text(m->command), (unsigned long long)group, (unsigned long long)server, (unsigned long long)m->owner, m->path); uasync_post(ua, member_files_gui_request, q); return JNI_TRUE; invalid: DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Android files: request rejected group=%llu server=%llu bytes=%d ready=%d", (unsigned long long)group, (unsigned long long)server, (int)size, inst && ua && uasync_is_running(ua)); u_free(q); return JNI_FALSE; } /* Копирование настоящего UTF-8 пути с проверкой длины и отсутствия NUL. */ static int android_file_path(JNIEnv* env, jbyteArray bytes, char* out) { jsize size = bytes ? (*env)->GetArrayLength(env, bytes) : 0; if (size < 1 || size > MF_PATH_MAX) return -1; (*env)->GetByteArrayRegion(env, bytes, 0, size, (jbyte*)out); if ((*env)->ExceptionCheck(env) || memchr(out, 0, size)) return -1; out[size] = 0; return 0; } /* Handle/локальный файл принадлежат общему transfer, Kotlin ждёт его терминальное событие. */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeFileTransfer( JNIEnv* env, jobject thiz, jlong id, jlong group, jlong server, jlong owner, jbyteArray remote, jbyteArray local, jboolean upload, jbyteArray version) { (void)thiz; struct UTUN_INSTANCE* inst = instance_lite_get_instance(); struct UASYNC* ua = instance_lite_get_uasync(); struct mf_transfer_request* q = NULL; if (!inst || !ua || !uasync_is_running(ua) || !id || !group || !server || !owner || !version || (*env)->GetArrayLength(env, version) != 16) goto invalid; q = u_calloc(1, sizeof(*q)); if (!q || android_file_path(env, remote, q->remote) || android_file_path(env, local, q->local) || !mf_path_valid(q->remote, 0) || q->local[0] != '/') goto invalid; (*env)->GetByteArrayRegion(env, version, 0, 16, (jbyte*)q->version); if ((*env)->ExceptionCheck(env) || (!upload && mf_zero(q->version, 16))) goto invalid; if (upload && (owner != (jlong)inst->node_id || (!strncasecmp(q->remote, "avatars", 7) && (!q->remote[7] || q->remote[7] == '/')))) goto invalid; q->inst = inst; q->id = id; q->group = group; q->server = server; q->owner = owner; q->upload = upload != JNI_FALSE; DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "Android files: transfer queued id=%llu group=%llu server=%llu owner=%llu upload=%d path=%s", (unsigned long long)id, (unsigned long long)group, (unsigned long long)server, (unsigned long long)owner, q->upload, q->remote); uasync_post(ua, member_files_gui_transfer, q); return JNI_TRUE; invalid: DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Android files: transfer rejected id=%llu group=%llu server=%llu owner=%llu upload=%d", (unsigned long long)id, (unsigned long long)group, (unsigned long long)server, (unsigned long long)owner, upload); u_free(q); return JNI_FALSE; } /* Отмена по уникальному ID не затрагивает передачи других владельцев. */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeFileCancel(JNIEnv* env, jobject thiz, jlong id) { (void)env; (void)thiz; struct UTUN_INSTANCE* inst = instance_lite_get_instance(); struct UASYNC* ua = instance_lite_get_uasync(); if (!id || !inst || !ua || !uasync_is_running(ua)) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "Android files: cancel unavailable id=%llu", (unsigned long long)id); return JNI_FALSE; } struct mf_cancel_request* q = u_calloc(1, sizeof(*q)); if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Android files: cancel allocation failed"); return JNI_FALSE; } q->inst = inst; q->id = id; DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "Android files: cancel queued id=%llu", (unsigned long long)id); uasync_post(ua, member_files_gui_cancel, q); return JNI_TRUE; } #endif /* __ANDROID__ */