Files
utun2/tools/chatgui/transport/gui_bridge_impl.cpp
T

857 lines
40 KiB
C++

#include "gui_bridge.h"
#include "chat/chat_event.h"
#include <QObject>
#include <QMetaObject>
#include <QByteArray>
#include <QCoreApplication>
#include <cstring>
#include <mutex>
#include <atomic>
extern "C" {
#include "../../../lib/u_async.h"
#include "../../../lib/mem.h"
#include "../../../lib/debug_config.h"
#include "../../../src/routing_layer/topo_node.h"
#include "../../../src/routing_layer/topo_group.h"
#include "../../../src/member_files/protocol.h"
#include "../../../src/chat/chat_member.h"
#include "../../../src/chat/chat_core.h"
#include "../../../src/chat/chat_sync.h"
#include "../../../src/chat/chat_join_port.h"
#include "../../../src/dm/dm_core.h"
}
/* ── Внутренний объект-приёмник в GUI-потоке ── */
class GuiBridgeReceiver : public QObject {
Q_OBJECT
public:
GuiBridgeReceiver(QObject* parent = nullptr) : QObject(parent) {}
Q_INVOKABLE void processPost(int eventType, QByteArray data);
};
static std::mutex bridge_mutex;
static struct chat_join_port* join_port = nullptr;
static std::atomic<uint64_t> join_sequence{1};
static GuiBridgeReceiver* g_receiver = nullptr;
static gui_connect_result_fn g_connect_result_cb = nullptr;
static gui_msg_received_fn g_msg_received_cb = nullptr;
static gui_channel_updated_fn g_channel_updated_cb = nullptr;
static gui_my_node_id_fn g_my_node_id_cb = nullptr;
static gui_auto_connect_status_fn g_auto_connect_status_cb = nullptr;
static gui_channel_peers_online_fn g_channel_peers_online_cb = nullptr;
static gui_db_ready_fn g_db_ready_cb = nullptr;
static gui_status_refresh_fn g_status_refresh_cb = nullptr;
static gui_cpu_load_fn g_cpu_load_cb = nullptr;
static gui_attachment_downloaded_fn g_attachment_downloaded_cb = nullptr;
static gui_download_progress_fn g_download_progress_cb = nullptr;
static gui_invite_received_fn g_invite_received_cb = nullptr;
static gui_conn_list_fn g_conn_list_cb = nullptr;
static gui_conn_metrics_fn g_conn_metrics_cb = nullptr;
static gui_member_detail_fn g_member_detail_cb = nullptr;
static gui_member_list_fn g_member_list_cb = nullptr;
static gui_member_updated_fn g_member_updated_cb = nullptr;
static gui_member_removed_fn g_member_removed_cb = nullptr;
static gui_admin_key_received_fn g_admin_key_received_cb = nullptr;
static gui_invite_link_ready_fn g_invite_link_ready_cb = nullptr;
static gui_invite_candidates_fn g_invite_candidates_cb = nullptr;
static gui_local_event_fn g_local_devices_cb = nullptr;
static gui_local_event_fn g_local_invite_cb = nullptr;
static gui_local_event_fn g_local_delivery_cb = nullptr;
static gui_call_incoming_fn g_call_incoming_cb = nullptr;
static gui_call_id_fn g_call_ringing_cb = nullptr;
static gui_call_id_fn g_call_phone_busy_cb = nullptr;
static gui_call_id_fn g_call_accepted_cb = nullptr;
static gui_call_ended_fn g_call_ended_cb = nullptr;
static gui_call_ended_fn g_call_declined_cb = nullptr;
static gui_call_error_fn g_call_error_cb = nullptr;
static gui_call_stats_fn g_call_stats_cb = nullptr;
static gui_call_path_fn g_call_path_cb = nullptr;
static gui_call_connection_fn g_call_connection_cb = nullptr;
static gui_radio_talk_fn g_radio_talk_cb = nullptr;
static gui_dm_msg_received_fn g_dm_msg_received_cb = nullptr;
static gui_dm_conv_updated_fn g_dm_conv_updated_cb = nullptr;
static gui_dm_messages_fn g_dm_messages_cb = nullptr;
static gui_dm_messages_fn g_attachment_preparation_cb = nullptr;
static gui_dm_messages_fn g_dm_call_route_cb = nullptr;
static gui_dm_messages_fn g_dm_invite_result_cb = nullptr;
static gui_dm_messages_fn g_dm_delete_result_cb = nullptr;
static gui_dm_messages_fn g_member_files_response_cb = nullptr;
static gui_dm_messages_fn g_member_files_transfer_cb = nullptr;
static gui_dm_messages_fn g_avatar_result_cb = nullptr;
static gui_dm_messages_fn g_group_avatar_result_cb = nullptr;
static gui_dm_messages_fn g_feedback_changed_cb = nullptr;
static gui_dm_messages_fn g_attachment_keys_cb = nullptr;
static gui_dm_messages_fn g_feedback_result_cb = nullptr;
static gui_dm_messages_fn g_msg_submitted_cb = nullptr;
static gui_dm_messages_fn g_msg_updated_cb = nullptr;
static gui_dm_messages_fn g_radio_mute_result_cb = nullptr;
static gui_dm_messages_fn g_radio_call_cb = nullptr;
static gui_dm_messages_fn g_stream_sources_cb = nullptr;
static gui_network_changed_fn g_network_changed_cb = nullptr;
static struct UASYNC* g_ua = nullptr;
static std::atomic<UTUN_INSTANCE*> g_inst{nullptr};
/* Зарезервированный post владеет своей записью; payload принадлежит мосту только при owned=true.
* Все ссылки списка защищены bridge_mutex. uasync освобождает post после callback/отмены. */
struct BridgePost {
struct posted_task post;
BridgePost* previous;
BridgePost* next;
void (*function)(void*);
void* arg;
bool owned, linked, cancelled;
};
static_assert(offsetof(BridgePost, post) == 0, "uasync must free the complete bridge post");
static BridgePost* pending_posts = nullptr;
/* Удаление за O(1), без ожидания исполнения функции пользователя под mutex моста. */
static void unlink_post(BridgePost* command) {
if (command->previous) command->previous->next = command->next;
else pending_posts = command->next;
if (command->next) command->next->previous = command->previous;
command->linked = false;
}
static void execute_post(void* arg) {
auto* command = static_cast<BridgePost*>(arg);
{
std::lock_guard<std::mutex> guard(bridge_mutex);
if (command->linked) unlink_post(command);
}
if (!command->cancelled) command->function(command->arg);
else if (command->owned) u_free(command->arg);
}
/* Вызывать под bridge_mutex: принятие команды и закрытие ядра образуют одну границу владения. */
static int post_command(void (*function)(void*), void* arg, bool owned) {
if (!function || !g_ua || !g_inst) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "GUI command rejected: core unavailable or missing callback"); return -1;
}
auto* command = static_cast<BridgePost*>(u_calloc(1, sizeof(BridgePost)));
if (!command) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "GUI command allocation failed"); return -1; }
command->function = function; command->arg = arg; command->owned = owned; command->linked = true;
command->next = pending_posts;
if (pending_posts) pending_posts->previous = command;
pending_posts = command;
command->post.callback = execute_post; command->post.arg = command;
uasync_post_reserved(g_ua, &command->post);
return 0;
}
/* Ядро снимается в своём потоке вне poll callbacks, до destroy instance и uasync. */
static void cancel_posts() {
unsigned cancelled = 0;
while (pending_posts) {
auto* command = pending_posts;
unlink_post(command); command->cancelled = true;
void* arg = command->arg;
const bool owned = command->owned;
if (uasync_cancel_post(g_ua, &command->post) == ERR_OK) {
if (owned) u_free(arg);
cancelled++;
} else {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "GUI command already dispatched during detach; deferred disposal");
}
}
if (cancelled) DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "GUI bridge detached: cancelled_commands=%u", cancelled);
}
/* ── GuiBridgeReceiver implementation ── */
void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
const uint8_t* d = (const uint8_t*)data.constData();
int dlen = data.size();
if (g_network_changed_cb && (eventType == 4 || eventType == 5 || eventType == 12 || eventType == 13 ||
eventType == 16 || eventType == 17 || eventType == 24 || eventType == 25 || eventType == 31 || eventType == 33))
g_network_changed_cb(eventType);
switch (eventType) {
case CHAT_EVT_MSG_UPDATED:
if (dlen < 34 || !d[0] || dlen != 1 + d[0] + 32) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "invalid message update bytes=%d", dlen); break;
}
if (g_msg_updated_cb) g_msg_updated_cb(d, dlen);
break;
case CHAT_EVT_RADIO_CALL:
if (g_radio_call_cb) g_radio_call_cb(d, dlen);
break;
case CHAT_EVT_LOCAL_DEVICES:
if (g_local_devices_cb) g_local_devices_cb(d, dlen);
break;
case CHAT_EVT_LOCAL_INVITE:
if (g_local_invite_cb) g_local_invite_cb(d, dlen);
break;
case CHAT_EVT_LOCAL_DELIVERY:
if (g_local_delivery_cb) g_local_delivery_cb(d, dlen);
break;
case CHAT_EVT_STREAM_SOURCES:
if (g_stream_sources_cb) g_stream_sources_cb(d, dlen);
break;
case CHAT_EVT_CPU_LOAD:
if (dlen != 2 || d[0] > 1 || d[1] > 100) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "invalid CPU load response bytes=%d", dlen);
if (g_cpu_load_cb) g_cpu_load_cb(0);
} else if (g_cpu_load_cb) g_cpu_load_cb(d[0] ? d[1] : 0);
break;
case CHAT_EVT_MSG_SUBMITTED:
if (dlen != 9 || d[8] > 1) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "invalid submission result bytes=%d", dlen); break; }
if (g_msg_submitted_cb) g_msg_submitted_cb(d, dlen);
break;
case CHAT_EVT_RADIO_MUTE_RESULT:
if (g_radio_mute_result_cb) g_radio_mute_result_cb(d, dlen);
break;
case CHAT_EVT_FEEDBACK_CHANGED:
if (g_feedback_changed_cb) g_feedback_changed_cb(d, dlen);
break;
case CHAT_EVT_ATTACHMENT_KEYS_CHANGED:
if (g_attachment_keys_cb) g_attachment_keys_cb(d, dlen);
break;
case CHAT_EVT_FEEDBACK_RESULT:
if (g_feedback_result_cb) g_feedback_result_cb(d, dlen);
break;
case CHAT_EVT_GROUP_AVATAR_RESULT:
if (g_group_avatar_result_cb) g_group_avatar_result_cb(d, dlen);
break;
case CHAT_EVT_AVATAR_RESULT:
if (g_avatar_result_cb) g_avatar_result_cb(d, dlen);
break;
case CHAT_EVT_MEMBER_FILES_RESPONSE:
if (g_member_files_response_cb) g_member_files_response_cb(d, dlen);
break;
case CHAT_EVT_MEMBER_FILES_TRANSFER:
if (g_member_files_transfer_cb) g_member_files_transfer_cb(d, dlen);
break;
case CHAT_EVT_DM_INVITE_RESULT:
if (g_dm_invite_result_cb) g_dm_invite_result_cb(d, dlen);
break;
case CHAT_EVT_DM_DELETE_RESULT:
if (dlen != 25 || d[24] > 1) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "gui_bridge: invalid DM delete result bytes=%d", dlen); break;
}
if (g_dm_delete_result_cb) g_dm_delete_result_cb(d, dlen);
break;
case CHAT_EVT_DM_CALL_ROUTE:
if (g_dm_call_route_cb) g_dm_call_route_cb(d, dlen);
break;
case GUI_EVT_MSG_RECEIVED:
if (dlen >= 2) {
uint8_t chLen = d[0];
if (chLen && dlen == 1 + chLen + 9 && d[1 + chLen + 8] <= 1) {
uint64_t author = 0;
memcpy(&author, d + 1 + chLen, 8);
const int initialSync = d[1 + chLen + 8];
QString chId = QString::fromUtf8((const char*)d + 1, chLen);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: MSG_RECEIVED ch=%s author=%llu initial_sync=%d",
qPrintable(chId), (unsigned long long)author, initialSync);
if (g_msg_received_cb) g_msg_received_cb((const char*)d + 1, chLen, author, initialSync);
} else {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "gui_bridge: MSG_RECEIVED invalid data length %d", dlen);
}
} else {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "gui_bridge: MSG_RECEIVED data too short %d", dlen);
}
break;
case GUI_EVT_CONNECT_RESULT:
if (dlen == 28) {
uint64_t nodeId, channelId; int result;
memcpy(&nodeId, d, 8); memcpy(&result, d + 8, 4); memcpy(&channelId, d + 12, 8);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "gui_bridge: CONNECT_RESULT node=0x%016llx ch=%llu result=%d",
(unsigned long long)nodeId, (unsigned long long)channelId, result);
uint64_t request; memcpy(&request, d + 20, 8);
if (g_connect_result_cb) g_connect_result_cb(nodeId, channelId, result, request);
} else {
DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "gui_bridge: CONNECT_RESULT data too short %d", dlen);
}
break;
case GUI_EVT_NEW_PEER:
if (dlen >= 8) {
uint64_t nodeId; memcpy(&nodeId, d, 8);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "gui_bridge: NEW_PEER node=0x%016llx", (unsigned long long)nodeId);
} else {
DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "gui_bridge: NEW_PEER data too short %d", dlen);
}
break;
case CHAT_EVT_CHANNEL_DELETED:
if (dlen < 2 || !d[0] || dlen != 1 + d[0]) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "gui_bridge: CHANNEL_DELETED invalid data length %d", dlen);
break;
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: CHANNEL_DELETED ch=%.*s", int(d[0]), (const char*)d + 1);
if (g_channel_updated_cb) g_channel_updated_cb((const char*)d + 1, d[0]);
break;
case GUI_EVT_CHANNEL_UPDATED:
if (dlen >= 2) {
uint8_t chLen = d[0];
if (dlen >= 1 + chLen) {
QString chId = QString::fromUtf8((const char*)d + 1, chLen);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: CHANNEL_UPDATED ch=%s", qPrintable(chId));
if (g_channel_updated_cb) g_channel_updated_cb((const char*)d + 1, chLen);
} else DEBUG_WARN(DEBUG_CATEGORY_CHAT, "gui_bridge: truncated CHANNEL_UPDATED bytes=%d channel_bytes=%u", dlen, chLen);
} else {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "gui_bridge: CHANNEL_UPDATED data too short %d", dlen);
}
break;
case GUI_EVT_MY_NODE_ID:
if (dlen >= 8) {
uint64_t nid; memcpy(&nid, d, 8);
if (g_my_node_id_cb) g_my_node_id_cb(nid);
} else DEBUG_WARN(DEBUG_CATEGORY_CHAT, "gui_bridge: truncated MY_NODE_ID bytes=%d", dlen);
break;
case GUI_EVT_AUTO_CONNECT_STATUS:
if (dlen >= 7) {
uint8_t status = d[0];
uint16_t total_tried; memcpy(&total_tried, d + 1, 2);
uint16_t node_count; memcpy(&node_count, d + 3, 2);
uint16_t connected; memcpy(&connected, d + 5, 2);
const char* st_str = status == 0 ? "in_progress" : status == 1 ? "done" : "stopped";
if (status != 0) DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "gui_bridge: AUTO_CONNECT %s tried=%u/%u connected=%u", st_str, total_tried, node_count, connected);
if (g_auto_connect_status_cb) g_auto_connect_status_cb(status, total_tried, node_count, connected);
} else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: truncated AUTO_CONNECT_STATUS bytes=%d", dlen);
break;
case GUI_EVT_CHANNEL_PEERS_ONLINE:
if (dlen >= 3) {
uint8_t chLen = d[0];
if (dlen >= 3 + chLen) {
uint16_t online; memcpy(&online, d + 1 + chLen, 2);
static char lastChId[256] = {0};
static uint16_t lastOnline = 0xFFFF;
if (chLen != (uint8_t)strlen(lastChId) || memcmp(lastChId, d + 1, chLen) != 0 || online != lastOnline) {
memcpy(lastChId, d + 1, chLen); lastChId[chLen] = '\0';
lastOnline = online;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: CHANNEL_PEERS_ONLINE ch=%.*s online=%u", chLen, (const char*)d + 1, online);
}
if (g_channel_peers_online_cb) g_channel_peers_online_cb((const char*)d + 1, chLen, online);
} else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: truncated CHANNEL_PEERS_ONLINE bytes=%d channel_bytes=%u", dlen, chLen);
} else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: missing CHANNEL_PEERS_ONLINE header bytes=%d", dlen);
break;
case GUI_EVT_DB_READY:
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: DB_READY (chat DB tables created)");
if (g_db_ready_cb) g_db_ready_cb();
break;
case GUI_EVT_STATUS_REFRESH:
if (g_status_refresh_cb) g_status_refresh_cb((const char*)d, dlen);
break;
case GUI_EVT_KEYS_GENERATED:
if (dlen == 64) DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "gui_bridge: local public key generated");
else DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "gui_bridge: invalid KEYS_GENERATED bytes=%d", dlen);
break;
case GUI_EVT_MEMBERS_CHANGED:
case CHAT_EVT_SERVICE_STARTED:
case CHAT_EVT_SERVICE_STOPPED:
case CHAT_EVT_LINK_STATUS_CHANGED:
// События сети уже переданы network_changed выше; отдельного payload callback нет.
break;
case GUI_EVT_ATTACHMENT_DOWNLOADED:
if (dlen >= 2) {
uint8_t chLen = d[0];
if (dlen >= 1 + chLen + 8) {
int64_t msgId = 0; memcpy(&msgId, d + 1 + chLen, 8);
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "gui_bridge: ATTACHMENT_DOWNLOADED ch=%.*s msgId=%lld", chLen, (const char*)d + 1, (long long)msgId);
if (g_attachment_downloaded_cb) g_attachment_downloaded_cb((const char*)d + 1, chLen, msgId);
} else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "gui_bridge: truncated ATTACHMENT_DOWNLOADED bytes=%d channel_bytes=%u", dlen, chLen);
} else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "gui_bridge: missing ATTACHMENT_DOWNLOADED header bytes=%d", dlen);
break;
case GUI_EVT_DOWNLOAD_PROGRESS:
if (dlen >= 2) {
uint8_t chLen = d[0];
if (dlen >= 1 + chLen + 16) {
int64_t msgId = 0; memcpy(&msgId, d + 1 + chLen, 8);
int32_t bd = 0, nb = 0; memcpy(&bd, d + 1 + chLen + 8, 4); memcpy(&nb, d + 1 + chLen + 12, 4);
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "gui_bridge: DOWNLOAD_PROGRESS ch=%.*s msgId=%lld %d/%d",
chLen, (const char*)d + 1, (long long)msgId, bd, nb);
if (g_download_progress_cb) g_download_progress_cb((const char*)d + 1, chLen, msgId, bd, nb);
} else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "gui_bridge: truncated DOWNLOAD_PROGRESS bytes=%d channel_bytes=%u", dlen, chLen);
} else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "gui_bridge: missing DOWNLOAD_PROGRESS header bytes=%d", dlen);
break;
case GUI_EVT_NODEINFO_UPDATE: break; /* network_changed уже уведомлён выше. */
case 18: /* CHAT_EVT_INVITE_RECEIVED */
if (g_invite_received_cb) g_invite_received_cb(d, dlen);
break;
case GUI_EVT_CONN_LIST:
if (g_conn_list_cb) g_conn_list_cb(d, dlen);
break;
case GUI_EVT_CONN_METRICS:
if (g_conn_metrics_cb) g_conn_metrics_cb(d, dlen);
break;
case GUI_EVT_MEMBER_DETAIL:
if (g_member_detail_cb) g_member_detail_cb(d, dlen);
break;
case GUI_EVT_MEMBER_LIST:
if (g_member_list_cb) g_member_list_cb(d, dlen);
break;
case GUI_EVT_MEMBER_UPDATED:
if (g_member_updated_cb) g_member_updated_cb(d, dlen);
break;
case GUI_EVT_MEMBER_REMOVED:
if (g_member_removed_cb) g_member_removed_cb(d, dlen);
break;
case GUI_EVT_ADMIN_KEY_RECEIVED:
if (dlen >= 2) {
uint8_t chLen = d[0];
if (dlen >= 1 + chLen + 8) {
uint64_t from; memcpy(&from, d + 1 + chLen, 8);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: ADMIN_KEY_RECEIVED ch=%.*s from=0x%016llx",
chLen, (const char*)d + 1, (unsigned long long)from);
if (g_admin_key_received_cb) g_admin_key_received_cb(d, dlen);
} else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: truncated ADMIN_KEY_RECEIVED bytes=%d channel_bytes=%u", dlen, chLen);
} else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: missing ADMIN_KEY_RECEIVED header bytes=%d", dlen);
break;
case GUI_EVT_INVITE_LINK_READY:
if (dlen >= 11) {
uint8_t chLen = d[0];
if (dlen < 1 + chLen + 10) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: truncated INVITE_LINK_READY bytes=%d channel_bytes=%u", dlen, chLen);
break;
}
uint64_t requestId; memcpy(&requestId, d + 1 + chLen, 8);
uint16_t linkLen; memcpy(&linkLen, d + 1 + chLen + 8, 2);
if (dlen >= 1 + chLen + 10 + linkLen) {
QByteArray link((const char*)d + 1 + chLen + 10, linkLen);
if (g_invite_link_ready_cb) g_invite_link_ready_cb((const char*)d + 1, chLen, requestId, link.constData());
} else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: truncated INVITE_LINK_READY bytes=%d link_bytes=%u", dlen, linkLen);
} else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: missing INVITE_LINK_READY header bytes=%d", dlen);
break;
case GUI_EVT_INVITE_CANDIDATES:
if (dlen >= 2) {
uint8_t chLen = d[0];
if (dlen >= 1 + chLen + 10) {
if (g_invite_candidates_cb) g_invite_candidates_cb((const char*)d + 1, chLen, d + 1 + chLen, dlen - 1 - chLen);
} else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: truncated INVITE_CANDIDATES bytes=%d channel_bytes=%u", dlen, chLen);
} else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: missing INVITE_CANDIDATES header bytes=%d", dlen);
break;
case GUI_EVT_DM_MSG_RECEIVED:
if (dlen >= 2) {
uint8_t cl = d[0];
if (dlen >= 1 + cl + 8) {
uint64_t author; memcpy(&author, d + 1 + cl, 8);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: DM_MSG_RECEIVED conv=%.*s author=%llu",
cl, (const char*)d + 1, (unsigned long long)author);
if (g_dm_msg_received_cb) g_dm_msg_received_cb((const char*)d + 1, cl, author);
} else DEBUG_WARN(DEBUG_CATEGORY_DM, "gui_bridge: truncated DM_MSG_RECEIVED bytes=%d conversation_bytes=%u", dlen, cl);
} else DEBUG_WARN(DEBUG_CATEGORY_DM, "gui_bridge: missing DM_MSG_RECEIVED header bytes=%d", dlen);
break;
case GUI_EVT_DM_CONV_UPDATED:
if (dlen >= 10 && d[0] && dlen == 9 + d[0]) {
uint64_t generation; memcpy(&generation, d + 1 + d[0], 8);
DEBUG_INFO(DEBUG_CATEGORY_DM, "gui_bridge: DM_CONV_UPDATED conv=%.*s generation=%llu",
d[0], (const char*)d + 1, (unsigned long long)generation);
if (g_dm_conv_updated_cb) g_dm_conv_updated_cb((const char*)d + 1, d[0], generation);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "gui_bridge: invalid DM conversation update bytes=%d", dlen);
}
break;
case GUI_EVT_DM_MESSAGES:
if (g_dm_messages_cb) g_dm_messages_cb(d, dlen);
break;
case CHAT_EVT_ATTACHMENT_PREPARATION:
if (g_attachment_preparation_cb) g_attachment_preparation_cb(d, dlen);
break;
case GUI_EVT_CALL_INCOMING:
if (dlen >= 1 + 8 + 8) {
uint8_t chLen = d[0];
if (dlen >= 1 + chLen + 8 + 8) {
uint64_t caller, callId;
memcpy(&caller, d + 1 + chLen, 8);
memcpy(&callId, d + 1 + chLen + 8, 8);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: CALL_INCOMING ch=%.*s caller=0x%016llx id=0x%016llx",
chLen, (const char*)d + 1, (unsigned long long)caller, (unsigned long long)callId);
if (g_call_incoming_cb) g_call_incoming_cb((const char*)d + 1, chLen, caller, callId);
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_INCOMING bytes=%d channel_bytes=%u", dlen, chLen);
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: missing CALL_INCOMING header bytes=%d", dlen);
break;
case GUI_EVT_CALL_RINGING:
if (dlen >= 8) {
uint64_t callId; memcpy(&callId, d, 8);
if (g_call_ringing_cb) g_call_ringing_cb(callId);
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_RINGING bytes=%d", dlen);
break;
case CHAT_EVT_CALL_PHONE_BUSY:
if (dlen == 9) {
uint64_t callId; memcpy(&callId, d, 8);
if (g_call_phone_busy_cb) g_call_phone_busy_cb(callId);
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: invalid PHONE_BUSY bytes=%d", dlen);
break;
case GUI_EVT_CALL_ACCEPTED:
if (dlen >= 8) {
uint64_t callId; memcpy(&callId, d, 8);
if (g_call_accepted_cb) g_call_accepted_cb(callId);
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_ACCEPTED bytes=%d", dlen);
break;
case GUI_EVT_CALL_ENDED:
if (dlen >= 9) {
uint64_t callId; memcpy(&callId, d, 8);
if (g_call_ended_cb) g_call_ended_cb(callId, d[8]);
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_ENDED bytes=%d", dlen);
break;
case GUI_EVT_CALL_DECLINED:
if (dlen >= 9) {
uint64_t callId; memcpy(&callId, d, 8);
if (g_call_declined_cb) g_call_declined_cb(callId, d[8]);
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_DECLINED bytes=%d", dlen);
break;
case GUI_EVT_CALL_ERROR:
if (dlen >= 9) {
uint64_t callId; memcpy(&callId, d, 8);
uint8_t err = d[8];
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: CALL_ERROR id=0x%016llx err=%u text=%.*s",
(unsigned long long)callId, (unsigned)err, dlen - 9, (const char*)d + 9);
if (g_call_error_cb) g_call_error_cb(callId, err, (const char*)d + 9, dlen - 9);
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_ERROR bytes=%d", dlen);
break;
case GUI_EVT_CALL_PATH:
if (dlen >= 9) {
uint64_t callId; memcpy(&callId, d, 8);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: CALL_PATH id=0x%016llx text=%.*s",
(unsigned long long)callId, dlen - 8, (const char*)d + 8);
if (g_call_path_cb) g_call_path_cb(callId, (const char*)d + 8, dlen - 8);
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_PATH bytes=%d", dlen);
break;
case GUI_EVT_CALL_CONNECTION:
if (dlen == 17 && d[8] <= 3) {
uint64_t callId, nextHop;
memcpy(&callId, d, 8); memcpy(&nextHop, d + 9, 8);
if (g_call_connection_cb) g_call_connection_cb(callId, d[8], nextHop);
} else {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: invalid CALL_CONNECTION size=%d", dlen);
}
break;
case GUI_EVT_CALL_STATS:
if (dlen == 24) {
uint64_t callId; memcpy(&callId, d, 8);
int values[8];
for (int i = 0; i < 8; i++) values[i] = (d[8 + i * 2] << 8) | d[9 + i * 2];
if (g_call_stats_cb) g_call_stats_cb(callId, values[0], values[1], values[2], values[3],
values[4], values[5], values[6], values[7]);
} else {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: invalid CALL_STATS size=%d", dlen);
}
break;
case GUI_EVT_RADIO_TALK:
if (dlen >= 19) {
uint64_t groupId, src;
memcpy(&groupId, d, 8);
memcpy(&src, d + 8, 8);
uint8_t on = d[18];
if (g_radio_talk_cb) g_radio_talk_cb(groupId, src, on);
} else DEBUG_WARN(DEBUG_CATEGORY_RADIO, "gui_bridge: truncated RADIO_TALK bytes=%d", dlen);
break;
default:
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "gui_bridge: unknown event type %d", eventType);
break;
}
}
/* ── C API ── */
extern "C" {
void gui_bridge_init(void) {
if (!QCoreApplication::instance()) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "GUI bridge requires QCoreApplication"); return; }
if (g_receiver) { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "GUI bridge already initialized"); return; }
g_receiver = new GuiBridgeReceiver(QCoreApplication::instance());
QObject::connect(g_receiver, &QObject::destroyed, [] { g_receiver = nullptr; });
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "GUI bridge initialized in GUI thread");
}
void gui_bridge_set_uasync(struct UASYNC* ua) {
std::lock_guard<std::mutex> guard(bridge_mutex);
if (g_inst && ua != g_ua) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "GUI bridge uasync replacement rejected: detach instance first"); return;
}
g_ua = ua;
}
void gui_bridge_set_inst(struct UTUN_INSTANCE* inst) {
struct chat_join_port* replacement = inst ? chat_sync_join_port(inst) : nullptr;
std::lock_guard<std::mutex> guard(bridge_mutex);
if (g_inst != inst) cancel_posts();
chat_join_port_release(join_port); join_port = replacement;
g_inst = inst;
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "GUI bridge instance %s: inst=%p ua=%p", inst ? "attached" : "detached", (void*)inst, (void*)g_ua);
}
int gui_bridge_join_link(const char* link, size_t len, uint64_t* request) {
if (!request) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "GUI join rejected: missing request output"); return CHAT_CONNECT_INVALID; }
std::lock_guard<std::mutex> guard(bridge_mutex);
uint64_t id = join_sequence.fetch_add(1);
int result = chat_join_port_submit(join_port, id, link, len);
if (!result) *request = id;
return result;
}
int gui_bridge_offer_link(uint64_t channel, const char* link, size_t len, uint64_t* request) {
if (!request) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "GUI offer rejected: missing request output"); return CHAT_CONNECT_INVALID; }
std::lock_guard<std::mutex> guard(bridge_mutex);
uint64_t id = join_sequence.fetch_add(1);
int result = chat_join_port_offer(join_port, id, channel, link, len);
if (!result) *request = id;
return result;
}
struct UTUN_INSTANCE* gui_bridge_get_inst(void) {
return g_inst;
}
void gui_bridge_post(int event_type, const uint8_t* data, int data_len) {
if (data_len < 0 || (data_len && !data)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "GUI event rejected: type=%d bytes=%d missing_data=%d", event_type, data_len, !data);
return;
}
if (!g_receiver) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "gui_bridge_post: g_receiver=NULL, dropping event %d", event_type); return; }
QByteArray d;
if (data_len > 0) d = QByteArray((const char*)data, data_len);
if (!QMetaObject::invokeMethod(g_receiver, "processPost",
Qt::QueuedConnection,
Q_ARG(int, event_type),
Q_ARG(QByteArray, d)))
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "GUI event queueing failed: type=%d bytes=%d", event_type, data_len);
}
void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg) {
std::lock_guard<std::mutex> guard(bridge_mutex);
post_command(fn, arg, false);
}
int gui_bridge_post_uasync_owned_fn(void (*fn)(void*), void* arg) {
std::lock_guard<std::mutex> guard(bridge_mutex);
return post_command(fn, arg, true);
}
int gui_bridge_submit_message(struct chat_msg_submit* req) {
std::lock_guard<std::mutex> guard(bridge_mutex);
if (!req || !g_inst || !g_ua || !uasync_is_running(g_ua)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "composer submission: core unavailable"); return -1;
}
req->inst = g_inst;
return post_command(chat_core_submit_trampoline, req, true);
}
int gui_bridge_is_uasync_ready(void) {
std::lock_guard<std::mutex> guard(bridge_mutex);
return g_ua && g_inst ? 1 : 0;
}
void gui_bridge_set_connect_result_cb(gui_connect_result_fn cb) {
g_connect_result_cb = cb;
}
void gui_bridge_set_msg_received_cb(gui_msg_received_fn cb) {
g_msg_received_cb = cb;
}
void gui_bridge_set_channel_updated_cb(gui_channel_updated_fn cb) {
g_channel_updated_cb = cb;
}
void gui_bridge_set_my_node_id_cb(gui_my_node_id_fn cb) {
g_my_node_id_cb = cb;
}
void gui_bridge_set_auto_connect_status_cb(gui_auto_connect_status_fn cb) {
g_auto_connect_status_cb = cb;
}
void gui_bridge_set_channel_peers_online_cb(gui_channel_peers_online_fn cb) {
g_channel_peers_online_cb = cb;
}
void gui_bridge_set_db_ready_cb(gui_db_ready_fn cb) {
g_db_ready_cb = cb;
}
void gui_bridge_set_status_refresh_cb(gui_status_refresh_fn cb) {
g_status_refresh_cb = cb;
}
void gui_bridge_set_cpu_load_cb(gui_cpu_load_fn cb) {
g_cpu_load_cb = cb;
}
int gui_bridge_request_cpu_load(void) {
std::lock_guard<std::mutex> guard(bridge_mutex);
if (!g_ua || !g_inst) return 0;
return post_command(chat_core_collect_cpu_load_trampoline, g_inst, false) == 0;
}
void gui_bridge_set_attachment_downloaded_cb(gui_attachment_downloaded_fn cb) {
g_attachment_downloaded_cb = cb;
}
void gui_bridge_set_download_progress_cb(gui_download_progress_fn cb) {
g_download_progress_cb = cb;
}
void gui_bridge_set_invite_received_cb(gui_invite_received_fn cb) {
g_invite_received_cb = cb;
}
void gui_bridge_set_conn_list_cb(gui_conn_list_fn cb) {
g_conn_list_cb = cb;
}
void gui_bridge_set_conn_metrics_cb(gui_conn_metrics_fn cb) {
g_conn_metrics_cb = cb;
}
void gui_bridge_set_member_detail_cb(gui_member_detail_fn cb) {
g_member_detail_cb = cb;
}
void gui_bridge_set_member_list_cb(gui_member_list_fn cb) {
g_member_list_cb = cb;
}
void gui_bridge_set_member_updated_cb(gui_member_updated_fn cb) {
g_member_updated_cb = cb;
}
void gui_bridge_set_member_removed_cb(gui_member_removed_fn cb) {
g_member_removed_cb = cb;
}
void gui_bridge_set_admin_key_received_cb(gui_admin_key_received_fn cb) {
g_admin_key_received_cb = cb;
}
void gui_bridge_set_invite_link_ready_cb(gui_invite_link_ready_fn cb) {
g_invite_link_ready_cb = cb;
}
void gui_bridge_set_invite_candidates_cb(gui_invite_candidates_fn cb) {
g_invite_candidates_cb = cb;
}
void gui_bridge_set_local_devices_cb(gui_local_event_fn cb) { g_local_devices_cb = cb; }
void gui_bridge_set_local_invite_cb(gui_local_event_fn cb) { g_local_invite_cb = cb; }
void gui_bridge_set_local_delivery_cb(gui_local_event_fn cb) { g_local_delivery_cb = cb; }
void gui_bridge_set_call_incoming_cb(gui_call_incoming_fn cb) {
g_call_incoming_cb = cb;
}
void gui_bridge_set_call_ringing_cb(gui_call_id_fn cb) {
g_call_ringing_cb = cb;
}
void gui_bridge_set_call_phone_busy_cb(gui_call_id_fn cb) { g_call_phone_busy_cb = cb; }
void gui_bridge_set_call_accepted_cb(gui_call_id_fn cb) {
g_call_accepted_cb = cb;
}
void gui_bridge_set_call_ended_cb(gui_call_ended_fn cb) {
g_call_ended_cb = cb;
}
void gui_bridge_set_call_declined_cb(gui_call_ended_fn cb) {
g_call_declined_cb = cb;
}
void gui_bridge_set_call_error_cb(gui_call_error_fn cb) {
g_call_error_cb = cb;
}
void gui_bridge_set_call_path_cb(gui_call_path_fn cb) {
g_call_path_cb = cb;
}
void gui_bridge_set_call_connection_cb(gui_call_connection_fn cb) {
g_call_connection_cb = cb;
}
void gui_bridge_set_radio_talk_cb(gui_radio_talk_fn cb) {
g_radio_talk_cb = cb;
}
void gui_bridge_set_dm_msg_received_cb(gui_dm_msg_received_fn cb) {
g_dm_msg_received_cb = cb;
}
void gui_bridge_set_network_changed_cb(gui_network_changed_fn cb) { g_network_changed_cb = cb; }
void gui_bridge_set_stream_sources_cb(gui_dm_messages_fn cb) { g_stream_sources_cb = cb; }
void gui_bridge_set_dm_call_route_cb(gui_dm_messages_fn cb) { g_dm_call_route_cb = cb; }
void gui_bridge_set_dm_invite_result_cb(gui_dm_messages_fn cb) { g_dm_invite_result_cb = cb; }
void gui_bridge_set_dm_delete_result_cb(gui_dm_messages_fn cb) { g_dm_delete_result_cb = cb; }
void gui_bridge_set_member_files_response_cb(gui_dm_messages_fn cb) { g_member_files_response_cb = cb; }
void gui_bridge_set_member_files_transfer_cb(gui_dm_messages_fn cb) { g_member_files_transfer_cb = cb; }
void gui_bridge_set_avatar_result_cb(gui_dm_messages_fn cb) { g_avatar_result_cb = cb; }
void gui_bridge_set_group_avatar_result_cb(gui_dm_messages_fn cb) { g_group_avatar_result_cb = cb; }
void gui_bridge_set_feedback_changed_cb(gui_dm_messages_fn cb) { g_feedback_changed_cb = cb; }
void gui_bridge_set_attachment_keys_cb(gui_dm_messages_fn cb) { g_attachment_keys_cb = cb; }
void gui_bridge_set_feedback_result_cb(gui_dm_messages_fn cb) { g_feedback_result_cb = cb; }
void gui_bridge_set_msg_submitted_cb(gui_dm_messages_fn cb) { g_msg_submitted_cb = cb; }
void gui_bridge_set_msg_updated_cb(gui_dm_messages_fn cb) { g_msg_updated_cb = cb; }
void gui_bridge_set_radio_mute_result_cb(gui_dm_messages_fn cb) { g_radio_mute_result_cb = cb; }
void gui_bridge_set_radio_call_cb(gui_dm_messages_fn cb) { g_radio_call_cb = cb; }
/* Резервируем post вместе с запросом: UI получает отказ, если поставить удаление в очередь невозможно. */
int gui_bridge_delete_dm(const char* conv_id, uint64_t generation, uint64_t request_id) {
std::lock_guard<std::mutex> guard(bridge_mutex);
if (!g_inst || !g_ua || !uasync_is_running(g_ua) || !conv_id || !conv_id[0] || strlen(conv_id) >= 64 ||
strspn(conv_id, "0123456789") != strlen(conv_id) || !generation || !request_id) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM delete: invalid request or stopped core generation=%llu request=%llu",
(unsigned long long)generation, (unsigned long long)request_id); return -1;
}
auto* req = static_cast<dm_delete_req*>(u_calloc(1, sizeof(dm_delete_req)));
if (!req) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM delete: request allocation failed"); return -1;
}
req->inst = g_inst; req->generation = generation; req->request_id = request_id; strcpy(req->conv_id, conv_id);
if (post_command(dm_delete_trampoline, req, true) < 0) { u_free(req); return -1; }
DEBUG_INFO(DEBUG_CATEGORY_DM, "DM delete queued: conv=%s generation=%llu request=%llu", conv_id,
(unsigned long long)generation, (unsigned long long)request_id);
return 0;
}
int gui_bridge_toggle_member_radio_mute(const char* channel_id, uint64_t node_id) {
std::lock_guard<std::mutex> guard(bridge_mutex);
const int running = g_inst && g_ua ? uasync_is_running(g_ua) : 0;
const bool valid_channel = channel_id && channel_id[0] && strlen(channel_id) < 64;
if (!g_inst || !running || !valid_channel || !node_id) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: GUI mute rejected inst=%p ua=%p running=%d valid_channel=%d node=%016llx",
(void*)g_inst.load(), (void*)g_ua, running, valid_channel, (unsigned long long)node_id); return -1;
}
auto* req = static_cast<chat_member_radio_mute_req*>(u_calloc(1, sizeof(chat_member_radio_mute_req)));
if (!req) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: GUI mute request allocation failed"); return -1;
}
req->inst = g_inst; req->node_id = node_id; strcpy(req->ch_id, channel_id);
if (post_command(chat_core_toggle_member_radio_mute_trampoline, req, true) < 0) { u_free(req); return -1; }
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio: GUI mute queued ch=%s node=%016llx", channel_id, (unsigned long long)node_id);
return 0;
}
void gui_bridge_set_dm_conv_updated_cb(gui_dm_conv_updated_fn cb) {
g_dm_conv_updated_cb = cb;
}
void gui_bridge_set_dm_messages_cb(gui_dm_messages_fn cb) {
g_dm_messages_cb = cb;
}
void gui_bridge_set_attachment_preparation_cb(gui_dm_messages_fn cb) {
g_attachment_preparation_cb = cb;
}
} /* extern "C" */
/* ── uTun nodeinfo callback (called from uasync thread) ── */
extern "C" void gui_nodeinfo_cb_impl(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq, void* arg) {
(void)group; (void)arg;
if (nq) gui_bridge_post(GUI_EVT_NODEINFO_UPDATE, nullptr, 0);
}
#include "gui_bridge_impl.moc"
void gui_bridge_set_call_stats_cb(gui_call_stats_fn cb) { g_call_stats_cb = cb; }