You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2510 lines
108 KiB
2510 lines
108 KiB
/* |
|
* 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 "../../../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/invite_build.h" |
|
#include "../../../src/chat/chat_setting.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 <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <stdarg.h> |
|
#include <unistd.h> |
|
|
|
#ifdef __ANDROID__ |
|
#include <jni.h> |
|
#include <android/log.h> |
|
#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) { |
|
const char* cat; |
|
switch (level) { |
|
case BLEV_TRACE: cat = "TRACE"; break; |
|
case BLEV_DEBUG: cat = "DEBUG"; break; |
|
case BLEV_INFO: cat = "INFO"; break; |
|
case BLEV_WARN: cat = "WARN"; break; |
|
case BLEV_ERROR: cat = "ERROR"; break; |
|
default: cat = "?"; break; |
|
} |
|
g_on_log(level, cat, fmt_buf); |
|
} |
|
} |
|
|
|
static void bridge_debug_hook(int level, const char* category, const char* message) { |
|
#ifdef __ANDROID__ |
|
__android_log_print(blevel_to_android(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 (g_on_log) g_on_log(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) { |
|
} |
|
|
|
void utun_bridge_send_message(const char* channel_id, const char* text) { |
|
bridge_log(BLEV_DEBUG, "send ch=%s text=%s", channel_id ? channel_id : "", text ? text : ""); |
|
if (!channel_id || !text) return; |
|
|
|
struct UASYNC* ua = instance_lite_get_uasync(); |
|
if (!uasync_is_running(ua)) return; |
|
|
|
size_t datalen = strlen(text); |
|
struct chat_msg_submit* req = u_calloc(1, sizeof(struct chat_msg_submit) + datalen); |
|
if (!req) return; |
|
|
|
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; |
|
|
|
uasync_post(ua, chat_core_submit_trampoline, req); |
|
bridge_log(BLEV_DEBUG, "send queued ch=%s len=%zu", channel_id, datalen); |
|
} |
|
|
|
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) { |
|
struct UTUN_INSTANCE* inst = instance_lite_get_instance(); |
|
sqlite3* db = chat_core_get_db(inst); |
|
if (!chat_core_is_initialized(inst) || !db || !channel_id || !channel_id[0]) return u_strdup("[]"); |
|
uint64_t my_id = inst->node_id; |
|
|
|
char tbl_msg[80]; |
|
msg_table_name(channel_id, tbl_msg, sizeof(tbl_msg)); |
|
|
|
char sql[512]; |
|
snprintf(sql, sizeof(sql), |
|
"SELECT id, node_id, data, timestamp, local_attrs FROM (" |
|
"SELECT id, node_id, data, timestamp, local_attrs, author_signature FROM \"%s\"" |
|
" ORDER BY timestamp DESC, author_signature DESC LIMIT %d" |
|
") ORDER BY timestamp ASC, author_signature ASC", tbl_msg, limit); |
|
|
|
sqlite3_stmt* st = NULL; |
|
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return u_strdup("[]"); |
|
|
|
size_t cap = 4096, pos = 1; |
|
char* json = u_malloc(cap); |
|
if (!json) { sqlite3_finalize(st); return u_strdup("[]"); } |
|
json[0] = '['; |
|
|
|
int first = 1; |
|
while (sqlite3_step(st) == SQLITE_ROW) { |
|
int64_t msg_id = sqlite3_column_int64(st, 0); |
|
int64_t node_id = sqlite3_column_int64(st, 1); |
|
int64_t ts = sqlite3_column_int64(st, 3); |
|
const uint8_t* dptr = sqlite3_column_blob(st, 2); |
|
int dlen = sqlite3_column_bytes(st, 2); |
|
const char* lattr = (const char*)sqlite3_column_text(st, 4); |
|
|
|
char txt[2048] = "", ct[64] = "text/plain"; |
|
if (dptr && dlen > 0) { |
|
const char* ds = strstr((const char*)dptr, "\"d\":\""); |
|
if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 2000) tl = 2000; memcpy(txt, ds, tl); txt[tl] = '\0'; } } |
|
const char* cts = strstr((const char*)dptr, "\"ct\":\""); |
|
if (cts) { cts += 6; char* cte = strchr((char*)cts, '"'); if (cte) { size_t tlc = (size_t)(cte - cts); if (tlc < sizeof(ct)) { memcpy(ct, cts, tlc); ct[tlc] = '\0'; } } } |
|
} |
|
/* base64-decode display prefix for media messages if marked with '*' (new format) */ |
|
if (strcmp(ct, "text/plain") != 0 && txt[0] == '*') { |
|
char* pipe = strchr(txt, '|'); |
|
if (pipe) { |
|
size_t prefix_len = (size_t)(pipe - txt); /* includes '*' */ |
|
uint8_t dec[1024]; int declen = (int)b64_decode(txt + 1, prefix_len - 1, dec, sizeof(dec)); |
|
if (declen > 0 && declen < 2000) { |
|
/* shift suffix left, place decoded at start */ |
|
size_t suffix_len = strlen(pipe); /* includes '|' */ |
|
memmove(txt + declen, pipe, suffix_len + 1); |
|
memcpy(txt, dec, (size_t)declen); |
|
} |
|
} |
|
} |
|
char* esc_txt = json_escape_alloc(txt); |
|
char* esc_ct = json_escape_alloc(ct); |
|
int is_out = (node_id == (int64_t)my_id) ? 1 : 0; |
|
const char* sep = first ? "" : ","; |
|
first = 0; |
|
|
|
char author_name[128]; |
|
bridge_get_node_name(db, (uint64_t)node_id, channel_id, author_name, sizeof(author_name)); |
|
if (!author_name[0]) |
|
snprintf(author_name, sizeof(author_name), "0x%016llx", (unsigned long long)node_id); |
|
|
|
/* build filePath for voice/attachment messages */ |
|
char file_path[1024] = ""; |
|
if (lattr) { |
|
const char* fps = strstr(lattr, "\"fp\":\""); |
|
if (fps) { fps += 6; |
|
char* fpe = strchr((char*)fps, '"'); |
|
if (fpe) { size_t fpl = (size_t)(fpe - fps); |
|
snprintf(file_path, sizeof(file_path), "%s/media/%s/%.*s", |
|
g_db_path, channel_id, (int)fpl, fps); |
|
} |
|
} |
|
} |
|
char* esc_fp = file_path[0] ? json_escape_alloc(file_path) : NULL; |
|
char* esc_la = lattr && lattr[0] ? json_escape_alloc(lattr) : NULL; |
|
|
|
size_t needed = snprintf(NULL, 0, "%s{\"id\":%lld,\"author\":\"%s\",\"text\":\"%s\",\"ts\":%lld,\"isOutgoing\":%s,\"contentType\":\"%s\"%s%s%s%s%s%s}", |
|
sep, (long long)msg_id, author_name, esc_txt, (long long)ts, is_out ? "true" : "false", esc_ct, |
|
esc_fp ? ",\"filePath\":\"" : "", esc_fp ? esc_fp : "", esc_fp ? "\"" : "", |
|
esc_la ? ",\"localAttrs\":\"" : "", esc_la ? esc_la : "", esc_la ? "\"" : ""); |
|
while (pos + needed + 2 > cap) { char* tmp = u_realloc(json, cap * 2); if (!tmp) break; json = tmp; cap *= 2; } |
|
if (pos + needed + 2 <= cap) |
|
pos += (size_t)snprintf(json + pos, cap - pos, "%s{\"id\":%lld,\"author\":\"%s\",\"text\":\"%s\",\"ts\":%lld,\"isOutgoing\":%s,\"contentType\":\"%s\"%s%s%s%s%s%s}", |
|
sep, (long long)msg_id, author_name, esc_txt, (long long)ts, is_out ? "true" : "false", esc_ct, |
|
esc_fp ? ",\"filePath\":\"" : "", esc_fp ? esc_fp : "", esc_fp ? "\"" : "", |
|
esc_la ? ",\"localAttrs\":\"" : "", esc_la ? esc_la : "", esc_la ? "\"" : ""); |
|
if (esc_la) u_free(esc_la); |
|
if (esc_fp) u_free(esc_fp); |
|
u_free(esc_ct); |
|
u_free(esc_txt); |
|
} |
|
sqlite3_finalize(st); |
|
|
|
json[pos++] = ']'; json[pos] = '\0'; |
|
return json; |
|
} |
|
|
|
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); |
|
} |
|
|
|
char* utun_bridge_dm_list_conversations_json(void) { |
|
struct UTUN_INSTANCE* inst = instance_lite_get_instance(); |
|
char buf[16384]; size_t len = 0; |
|
if (!inst || dm_list_conversations_json(inst, buf, sizeof(buf), &len) < 0) |
|
return u_strdup("[]"); |
|
return u_strdup(buf); |
|
} |
|
|
|
char* utun_bridge_dm_list_messages_json(const char* conv_id, int count, int offset) { |
|
struct UTUN_INSTANCE* inst = instance_lite_get_instance(); |
|
if (!inst || !conv_id || !conv_id[0]) return u_strdup("[]"); |
|
size_t cap = 262144; |
|
char* buf = u_malloc(cap); |
|
if (!buf) { bridge_log(BLEV_ERROR, "dm_list_messages: OOM"); return u_strdup("[]"); } |
|
size_t len = 0; |
|
if (dm_list_messages_json(inst, conv_id, count, offset, buf, cap, &len) < 0) { |
|
u_free(buf); |
|
return u_strdup("[]"); |
|
} |
|
char* out = u_strdup(buf); |
|
u_free(buf); |
|
return out; |
|
} |
|
|
|
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 <openssl/evp.h> |
|
|
|
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)" |
|
" 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 = 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; |
|
} |
|
|
|
char* esc_name = json_escape_alloc(local_nick && local_nick[0] ? local_nick : |
|
(name && name[0] ? name : "")); |
|
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}", |
|
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); |
|
|
|
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}", |
|
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); |
|
u_free(esc_name); |
|
} |
|
sqlite3_finalize(st); |
|
|
|
json[pos++] = ']'; json[pos] = '\0'; |
|
return json; |
|
} |
|
|
|
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 : ""); |
|
pos += (size_t)snprintf(buf + pos, sizeof(buf) - pos, |
|
",\"n\":\"%s\",\"on\":0,\"ls\":%lld,\"cr\":%lld", |
|
esc_name, (long long)last_seen, (long long)created); |
|
u_free(esc_name); |
|
} |
|
sqlite3_finalize(st); |
|
} |
|
|
|
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); |
|
snprintf(buf + pos, sizeof(buf) - pos, ",\"adr\":["); pos += strlen(buf + pos); |
|
|
|
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; } |
|
pos += (size_t)snprintf(buf + pos, 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); |
|
af = 0; |
|
} |
|
sqlite3_finalize(st); |
|
} |
|
|
|
pos += (size_t)snprintf(buf + pos, sizeof(buf) - pos, "]}"); |
|
return u_strdup(buf); |
|
} |
|
|
|
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; |
|
utun_set_client_activity(inst, active); |
|
standby_set_enabled(inst, !active); |
|
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) { |
|
voice_recorder_init(g_db_path); /* idempotent: гарантирует g_rec после stop/restart */ |
|
return voice_recorder_start(channel_id, 48000, 1); |
|
} |
|
|
|
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) { |
|
return attachment_send(channel_id, file_path, db_path); |
|
} |
|
|
|
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) { |
|
return video_send(channel_id, file_path, db_path, duration_ms, width, height); |
|
} |
|
|
|
int utun_bridge_photo_send(const char* channel_id, const char* file_path, const char* db_path, |
|
int width, int height) { |
|
return photo_send(channel_id, file_path, db_path, width, height); |
|
} |
|
|
|
/* ────────────────────────────────────────────────────────────────── |
|
* 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) { fclose(f); return NULL; } |
|
if (fread(&ch, 2, 1, f) != 1) { fclose(f); return NULL; } |
|
if (fread(&fs, 2, 1, f) != 1) { fclose(f); return NULL; } |
|
|
|
opus_codec_decoder_t* dec = opus_codec_decoder_create((int)sr, (int)ch); |
|
if (!dec) { fclose(f); return NULL; } |
|
|
|
struct voice_decode_handle* h = u_calloc(1, sizeof(*h)); |
|
if (!h) { 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; |
|
if (fread(&plen, 2, 1, f) != 1 || plen == 0) break; |
|
fseek(f, plen, SEEK_CUR); |
|
h->total_samples += (int)fs * (int)ch; |
|
} |
|
fseek(f, pos, SEEK_SET); |
|
|
|
*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; |
|
} |
|
|
|
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) break; |
|
if (plen > sizeof(packet)) { fseek(h->file, plen, SEEK_CUR); break; } |
|
if (fread(packet, 1, plen, h->file) != plen) break; |
|
|
|
int decoded = opus_codec_decode(h->dec, packet, plen, buf + written, h->frame_samples); |
|
if (decoded < 0) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "voice_decode_read: error %d", decoded); break; } |
|
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_vad_mode(void) { |
|
return radio_audio_vad_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) { |
|
bridge_log(BLEV_DEBUG, "[EVT_DIAG] event type=%d len=%d", type, 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; |
|
udp_log_cancel_timer(); |
|
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; } |
|
} |
|
|
|
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSendMessage( |
|
JNIEnv* env, jobject thiz, jstring channelId, jstring text) { |
|
(void)thiz; |
|
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); |
|
const char* tx = (*env)->GetStringUTFChars(env, text, NULL); |
|
utun_bridge_send_message(ch, tx); |
|
if (tx) (*env)->ReleaseStringUTFChars(env, text, tx); |
|
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); |
|
} |
|
|
|
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) { |
|
(void)thiz; |
|
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); |
|
char* json = utun_bridge_get_messages_json(ch, (int)limit); |
|
jstring result = (*env)->NewStringUTF(env, json); |
|
u_free(json); |
|
(*env)->ReleaseStringUTFChars(env, channelId, ch); |
|
return result; |
|
} |
|
|
|
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); |
|
} |
|
|
|
JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeDmListConversations( |
|
JNIEnv* env, jobject thiz) { |
|
(void)thiz; |
|
char* json = utun_bridge_dm_list_conversations_json(); |
|
jstring result = (*env)->NewStringUTF(env, json); |
|
u_free(json); |
|
return result; |
|
} |
|
|
|
JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeDmListMessages( |
|
JNIEnv* env, jobject thiz, jstring convId, jint count, jint offset) { |
|
(void)thiz; |
|
const char* c = (*env)->GetStringUTFChars(env, convId, NULL); |
|
char* json = utun_bridge_dm_list_messages_json(c, (int)count, (int)offset); |
|
jstring result = (*env)->NewStringUTF(env, json); |
|
u_free(json); |
|
if (c) (*env)->ReleaseStringUTFChars(env, convId, c); |
|
return result; |
|
} |
|
|
|
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(); |
|
} |
|
|
|
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) { |
|
(void)thiz; |
|
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); |
|
int rc = utun_bridge_voice_start(ch); |
|
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; |
|
utun_bridge_voice_stop(&duration_ms); |
|
return (jint)duration_ms; |
|
} |
|
|
|
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) { |
|
(void)thiz; |
|
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); |
|
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) { |
|
(void)thiz; |
|
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); |
|
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) { |
|
(void)thiz; |
|
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); |
|
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 ── */ |
|
|
|
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 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, jboolean vad) { |
|
(void)env; (void)thiz; |
|
return radio_audio_capture_start((uint64_t)groupId, (int)channels, vad ? 1 : 0) == 0 ? JNI_TRUE : JNI_FALSE; |
|
} |
|
|
|
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 jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioVadMode( |
|
JNIEnv* env, jobject thiz) { |
|
(void)env; (void)thiz; |
|
return utun_bridge_radio_vad_mode() ? JNI_TRUE : JNI_FALSE; |
|
} |
|
|
|
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioAudioPull( |
|
JNIEnv* env, jobject thiz, jlong groupId, 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_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); |
|
} |
|
|
|
#endif /* __ANDROID__ */
|
|
|