diff --git a/build.bat b/build.bat index c6416ad4..147b0d35 100644 --- a/build.bat +++ b/build.bat @@ -28,4 +28,4 @@ if not exist "%MSYS2_PATH%\msys2_shell.cmd" ( REM Launch MSYS2 UCRT64 shell and run build script echo Starting MSYS2 UCRT64 environment... rem call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -c "bash build.sh > build_win.log 2>&1" -call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "bash build.sh 2>&1 | tee build_win.log" +call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh 2>&1 | tee build_win.log" diff --git a/build_full.bat b/build_full.bat index 9d0ce429..edcab3cd 100644 --- a/build_full.bat +++ b/build_full.bat @@ -30,4 +30,4 @@ REM Launch MSYS2 UCRT64 shell and run build script with --full flag echo Starting MSYS2 UCRT64 environment for FULL REBUILD... echo Running: bash build.sh --full echo. -call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "bash build.sh --full 2>&1 | tee build_full.log" +call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh --full 2>&1 | tee build_full.log" diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index b6f79c80..2d06c94c 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -61,6 +61,7 @@ add_executable(chatgui src/qrcode_utils.cpp src/invite_link.cpp src/invitedialog.cpp + src/joindialog.cpp transport/msg_client.cpp transport/crypto.cpp transport/utun_node.cpp @@ -74,7 +75,11 @@ add_executable(chatgui target_include_directories(chatgui PRIVATE ${CMAKE_SOURCE_DIR}/db) target_compile_definitions(chatgui PRIVATE SQLITE_THREADSAFE=1) set_source_files_properties(db/sqlite3.c PROPERTIES LANGUAGE C) -target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB OpenSSL::Crypto ZXing::ZXing utun pthread dl) +if(WIN32) + target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB OpenSSL::Crypto ZXing::ZXing utun pthread) +else() + target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB OpenSSL::Crypto ZXing::ZXing utun pthread dl) +endif() # ============================================================================ # Tests diff --git a/tools/chatgui/build.bat b/tools/chatgui/build.bat index 15c76363..6209cbd4 100644 --- a/tools/chatgui/build.bat +++ b/tools/chatgui/build.bat @@ -23,7 +23,7 @@ echo. set "CURRENT_DIR=%CD%" -call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "cd \"%CURRENT_DIR%/tools/chatgui\" && bash build.sh 2>&1 | tee build_win.log" +call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; cd \"%CURRENT_DIR%/tools/chatgui\" && bash build.sh 2>&1 | tee build_win.log" set BUILD_RESULT=%ERRORLEVEL% diff --git a/tools/chatgui/build.sh b/tools/chatgui/build.sh index 83d2794d..da946fd2 100755 --- a/tools/chatgui/build.sh +++ b/tools/chatgui/build.sh @@ -28,7 +28,11 @@ cmake --build "$BUILD_DIR" 2>&1 | tee -a "$LOG_FILE" test ${PIPESTATUS[0]} -eq 0 || { log "Build FAILED"; exit 1; } log "" -log "[3/3] Verifying..." +log "[3/3] Copying runtime DLLs..." +cp "$SRC_DIR/rlottie_build/librlottie.dll" "$BUILD_DIR/" 2>/dev/null && log " OK: librlottie.dll" || log " WARN: librlottie.dll not found" + +log "" +log "[4/4] Verifying..." EXE="$BUILD_DIR/chatgui.exe" if [ -f "$EXE" ]; then SIZE=$(du -h "$EXE" | cut -f1) diff --git a/tools/chatgui/src/channellist.cpp b/tools/chatgui/src/channellist.cpp index 911a8400..53767319 100644 --- a/tools/chatgui/src/channellist.cpp +++ b/tools/chatgui/src/channellist.cpp @@ -2,7 +2,9 @@ #include "channeldelegate.h" #include "../db/db_manager.h" #include +#include #include +#include #include #include #include @@ -49,6 +51,31 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) m_listView->setContextMenuPolicy(Qt::CustomContextMenu); layout->addWidget(m_listView); + auto *footer = new QWidget(this); + auto *fl = new QHBoxLayout(footer); + fl->setContentsMargins(6, 4, 6, 4); + fl->setSpacing(6); + + auto *gearBtn = new QPushButton(QString::fromUtf8("\xE2\x9A\x99"), footer); + gearBtn->setFixedSize(28, 28); + gearBtn->setStyleSheet( + "QPushButton { border: none; font-size: 18px; background: transparent; color: gray; }" + "QPushButton:hover { color: palette(text); }"); + fl->addWidget(gearBtn); + + fl->addStretch(); + + auto *plusBtn = new QPushButton("+", footer); + plusBtn->setFixedSize(28, 28); + plusBtn->setStyleSheet( + "QPushButton { border: 1.5px solid #3390EC; border-radius: 14px;" + " font-size: 18px; font-weight: bold; color: #3390EC; background: transparent; }" + "QPushButton:hover { background: #3390EC; color: white; }"); + connect(plusBtn, &QPushButton::clicked, this, [this] { emit joinRequested(); }); + fl->addWidget(plusBtn); + + layout->addWidget(footer); + connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex ¤t, const QModelIndex &) { if (current.isValid()) { diff --git a/tools/chatgui/src/channellist.h b/tools/chatgui/src/channellist.h index 6e4cfa2c..fdce4308 100644 --- a/tools/chatgui/src/channellist.h +++ b/tools/chatgui/src/channellist.h @@ -18,6 +18,7 @@ public: signals: void channelSelected(const QString& channelId); void inviteRequested(const QString& channelId); + void joinRequested(); private slots: void onContextMenu(const QPoint& pos); diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 9a941960..7a444d75 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -3,6 +3,7 @@ #include "messagelist.h" #include "accountlist.h" #include "invitedialog.h" +#include "joindialog.h" #include "invite_link.h" #include "../db/db_manager.h" #include "../transport/utun_node.h" @@ -42,6 +43,8 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db) connect(m_channelList, &ChannelList::inviteRequested, this, &MainWindow::showInviteDialog); + connect(m_channelList, &ChannelList::joinRequested, + this, &MainWindow::showJoinDialog); m_channelList->loadChannels(); m_accountList->loadAccounts(); @@ -179,3 +182,14 @@ void MainWindow::showInviteDialog(const QString& channelId) { InviteDialog dlg(link, this); dlg.exec(); } + +void MainWindow::showJoinDialog() { + if (!m_node || !m_node->isRunning()) { + QMessageBox::warning(this, "Error", "Node is not running."); + return; + } + JoinDialog dlg(m_node, m_db, this); + if (dlg.exec() == QDialog::Accepted) { + m_channelList->loadChannels(); + } +} diff --git a/tools/chatgui/src/mainwindow.h b/tools/chatgui/src/mainwindow.h index 143253df..eeb7ccb6 100644 --- a/tools/chatgui/src/mainwindow.h +++ b/tools/chatgui/src/mainwindow.h @@ -23,6 +23,7 @@ protected: private slots: void onTrayActivated(QSystemTrayIcon::ActivationReason reason); void showInviteDialog(const QString& channelId); + void showJoinDialog(); private: void setupUi(); diff --git a/tools/chatgui/transport/chat_sync.c b/tools/chatgui/transport/chat_sync.c index 9e6b2d23..5f90d7aa 100644 --- a/tools/chatgui/transport/chat_sync.c +++ b/tools/chatgui/transport/chat_sync.c @@ -658,3 +658,134 @@ void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id) { uint8_t req[8]; memcpy(req, &node_id, 8); gui_bridge_call(GUI_OP_LOAD_NODEINFO, req, 8, NULL, NULL); } + +/* ── Invite link connect ── */ + +struct cs_invite_arg { + uint64_t node_id; + uint8_t pubkey[SC_PUBKEY_SIZE]; + uint8_t* addrs_data; + int addr_count; + uint8_t* ch_id; + int ch_id_len; +}; + +static void cs_connect_result_cb(int result, uint64_t node_id, void* arg) { + (void)arg; + uint8_t data[12]; + memcpy(data, &node_id, 8); + memcpy(data + 8, &result, 4); + gui_bridge_post(GUI_EVT_CONNECT_RESULT, data, 12); +} + +static void cs_connect_from_invite_impl(void* arg) { + struct cs_invite_arg* a = (struct cs_invite_arg*)arg; + uint64_t node_id = a->node_id; + if (!g_cs || !g_cs->inst || !g_cs->inst->bgp || !g_cs->inst->conn_mgr) { + uint8_t err[12]; int r = -7; + memcpy(err, &node_id, 8); memcpy(err + 8, &r, 4); + gui_bridge_post(GUI_EVT_CONNECT_RESULT, err, 12); + goto cleanup; + } + + struct ROUTE_BGP* bgp = g_cs->inst->bgp; + if (nodeinfo_find_by_id(bgp, node_id)) { + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "chat_sync: node 0x%016llx already in BGP, connecting", (unsigned long long)node_id); + conn_mgr_connect_node(g_cs->inst->conn_mgr, node_id, 30000, cs_connect_result_cb, NULL); + goto cleanup; + } + + size_t dyn_sz = (size_t)a->addr_count * sizeof(struct NODEINFO_IPV4_ADDR); + size_t data_sz = sizeof(struct NODEINFO_Q) - sizeof(struct ll_entry) + dyn_sz + 8; + struct NODEINFO_Q* nq = (struct NODEINFO_Q*)queue_entry_new(data_sz); + if (!nq) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "chat_sync: failed to alloc NODEINFO_Q"); + uint8_t err[12]; int r = -7; + memcpy(err, &node_id, 8); memcpy(err + 8, &r, 4); + gui_bridge_post(GUI_EVT_CONNECT_RESULT, err, 12); + goto cleanup; + } + + struct NODEINFO* ni = &nq->node; + ni->node_id = node_id; + ni->ver = 0; + memcpy(ni->public_key, a->pubkey, SC_PUBKEY_SIZE); + memset(ni->ed25519_public_key, 0, SC_PUBKEY_SIZE); + ni->node_name_len = 0; + ni->local_v4_sockets = 0; + ni->local_v4_addrs = (uint8_t)a->addr_count; + ni->local_v6_sockets = 0; + ni->local_v6_addrs = 0; + ni->local_v4_subnets = 0; + ni->local_v6_subnets = 0; + ni->tranzit_nodes = 0; + ni->hop_count = 0; + + uint8_t* dyn = (uint8_t*)ni + sizeof(struct NODEINFO); + const uint8_t* src = a->addrs_data; + for (int i = 0; i < a->addr_count; i++) { + struct NODEINFO_IPV4_ADDR* addr = (struct NODEINFO_IPV4_ADDR*)(dyn + i * sizeof(struct NODEINFO_IPV4_ADDR)); + memset(addr, 0, sizeof(*addr)); + uint8_t family = *src++; + if (family == 4) { + memcpy(addr->addr, src, 4); src += 4; + uint16_t port_be = ((uint16_t)src[0] << 8) | src[1]; src += 2; + addr->port = port_be; + addr->type = ADDR_TYPE_REAL; + addr->socket_id = 0; + addr->protocol = NODE_PROTO_UDP; + } else { + src += 18; + continue; + } + } + + nq->alien = 0; + nq->dirty = 0; + nq->last_ver = 0; + nq->conn_mgr_type = CONN_TYPE_NONE; + nq->conn_mgr_intermediariy_count = 0; + memset(&nq->connectivity, 0, sizeof(nq->connectivity)); + nq->best_socket = NULL; + + queue_data_put_with_index(bgp->nodes, &nq->ll); + + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "chat_sync: created NODEINFO for 0x%016llx, %d addrs, connecting", + (unsigned long long)node_id, a->addr_count); + conn_mgr_connect_node(g_cs->inst->conn_mgr, node_id, 30000, cs_connect_result_cb, NULL); + +cleanup: + u_free(a->addrs_data); + u_free(a->ch_id); + u_free(a); +} + +void chat_sync_connect_from_invite(uint64_t node_id, const uint8_t* pubkey_bin, + const uint8_t* addrs_data, int addr_count, + const uint8_t* channel_id, int ch_id_len) { + if (!g_cs || !g_cs->inst || !g_cs->inst->ua) { + int r = -7; + uint8_t err[12]; memcpy(err, &node_id, 8); memcpy(err + 8, &r, 4); + gui_bridge_post(GUI_EVT_CONNECT_RESULT, err, 12); + return; + } + + struct cs_invite_arg* a = u_calloc(1, sizeof(struct cs_invite_arg)); + if (!a) return; + + a->node_id = node_id; + memcpy(a->pubkey, pubkey_bin, SC_PUBKEY_SIZE); + + size_t addrs_sz = (size_t)addr_count * 7; + a->addrs_data = u_malloc(addrs_sz); + if (!a->addrs_data) { u_free(a); return; } + memcpy(a->addrs_data, addrs_data, addrs_sz); + a->addr_count = addr_count; + + a->ch_id = u_malloc((size_t)ch_id_len + 1); + if (!a->ch_id) { u_free(a->addrs_data); u_free(a); return; } + memcpy(a->ch_id, channel_id, (size_t)ch_id_len); + a->ch_id_len = ch_id_len; + + gui_bridge_post_uasync(g_cs->inst->ua, cs_connect_from_invite_impl, a); +} diff --git a/tools/chatgui/transport/chat_sync.h b/tools/chatgui/transport/chat_sync.h index a20d9c8e..14b38051 100644 --- a/tools/chatgui/transport/chat_sync.h +++ b/tools/chatgui/transport/chat_sync.h @@ -47,6 +47,11 @@ int chat_sync_push(struct UTUN_INSTANCE* inst, /* Прослойка conn_mgr: загрузить nodeinfo из БД и подключиться */ void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id); +/* Подключиться к пиру по данным invite-ссылки (вызывается из GUI-потока) */ +void chat_sync_connect_from_invite(uint64_t node_id, const uint8_t* pubkey_bin, + const uint8_t* addrs_data, int addr_count, + const uint8_t* channel_id, int ch_id_len); + #ifdef __cplusplus } #endif diff --git a/tools/chatgui/transport/gui_bridge.h b/tools/chatgui/transport/gui_bridge.h index 03e8b825..d9c5af3d 100644 --- a/tools/chatgui/transport/gui_bridge.h +++ b/tools/chatgui/transport/gui_bridge.h @@ -54,6 +54,10 @@ void gui_bridge_post(int event_type, const uint8_t* data, int data_len); /* GUI → uasync: выполнить функцию в uasync-потоке */ void gui_bridge_post_uasync(struct UASYNC* ua, void (*fn)(void*), void* arg); +/* Callback for connect results (called from GUI thread) */ +typedef void (*gui_connect_result_fn)(uint64_t node_id, int result); +void gui_bridge_set_connect_result_cb(gui_connect_result_fn cb); + #ifdef __cplusplus } #endif diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index 719a9922..f54807dc 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -29,6 +29,7 @@ public: }; static GuiBridgeReceiver* g_receiver = nullptr; +static gui_connect_result_fn g_connect_result_cb = nullptr; /* ── Трамплин для uasync_post: распаковывает результат и вызывает оригинальный callback ── */ @@ -190,6 +191,7 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { uint64_t nodeId; int result; memcpy(&nodeId, d, 8); memcpy(&result, d + 8, 4); db->setNodeOnline(nodeId, result == 0); + if (g_connect_result_cb) g_connect_result_cb(nodeId, result); } break; case GUI_EVT_NEW_PEER: @@ -247,6 +249,10 @@ void gui_bridge_post_uasync(struct UASYNC* ua, void (*fn)(void*), void* arg) { if (ua && fn) uasync_post(ua, fn, arg); } +void gui_bridge_set_connect_result_cb(gui_connect_result_fn cb) { + g_connect_result_cb = cb; +} + } /* extern "C" */ #include "gui_bridge_impl.moc"