#include "joindialog.h" #include "invite_link.h" #include "debug_ui.h" #include "../transport/utun_node.h" #include "../transport/gui_bridge.h" #include "chat/chat_sync.h" #include "../db/db_manager.h" #include #include #include #include #include #include #include extern "C" { #include "secure_channel.h" } JoinDialog::JoinDialog(UtunNode* node, DbManager* db, QWidget* parent, const QString& autoJoinLink) : QDialog(parent) , m_node(node) , m_db(db) { setWindowTitle("Join Channel"); setMinimumWidth(420); setModal(true); auto *layout = new QVBoxLayout(this); layout->setSpacing(12); layout->setContentsMargins(20, 16, 20, 16); auto *titleLabel = new QLabel("Paste invite link:", this); titleLabel->setStyleSheet("font-weight: bold; font-size: 13px;"); layout->addWidget(titleLabel); m_linkEdit = new QLineEdit(this); m_linkEdit->setPlaceholderText("utun://..."); m_linkEdit->setStyleSheet( "QLineEdit { border: 1px solid palette(mid); border-radius: 6px;" " padding: 8px 12px; font-size: 13px; background: palette(base); }"); layout->addWidget(m_linkEdit); m_statusLabel = new QLabel(this); m_statusLabel->setWordWrap(true); m_statusLabel->setStyleSheet("color: gray; font-size: 12px; padding: 4px 0;"); layout->addWidget(m_statusLabel); layout->addStretch(); auto *btnLayout = new QHBoxLayout(); btnLayout->setSpacing(8); btnLayout->addStretch(); m_cancelBtn = new QPushButton("Cancel", this); m_cancelBtn->setFixedWidth(90); m_cancelBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 16px; font-size: 13px; background: palette(button); }" "QPushButton:hover { background: palette(light); }"); connect(m_cancelBtn, &QPushButton::clicked, this, &QDialog::reject); btnLayout->addWidget(m_cancelBtn); m_connectBtn = new QPushButton("Connect", this); m_connectBtn->setFixedWidth(90); m_connectBtn->setDefault(true); m_connectBtn->setStyleSheet( "QPushButton { border: none; border-radius: 4px;" " padding: 6px 16px; font-size: 13px; font-weight: bold;" " background: #3390EC; color: white; }" "QPushButton:hover { background: #2B7ED4; }" "QPushButton:disabled { background: palette(mid); color: palette(midlight); }"); connect(m_connectBtn, &QPushButton::clicked, this, &JoinDialog::onConnectClicked); btnLayout->addWidget(m_connectBtn); layout->addLayout(btnLayout); connect(m_linkEdit, &QLineEdit::returnPressed, this, &JoinDialog::onConnectClicked); connect(m_linkEdit, &QLineEdit::textChanged, this, &JoinDialog::onLinkTextChanged); m_decodeTimer = new QTimer(this); m_decodeTimer->setSingleShot(true); m_decodeTimer->setInterval(400); connect(m_decodeTimer, &QTimer::timeout, this, [this]() { QString link = m_linkEdit->text().trimmed(); if (link.isEmpty()) { setStatus(""); return; } InviteData d = decodeInviteLink(link); if (!d.error.isEmpty() || d.pubkey.size() != 32) { setStatus(""); return; } uint64_t nodeId = sc_derive_node_id_from_pubkey((const uint8_t*)d.pubkey.constData()); QStringList addrList; for (const auto& a : d.addrs) { QString addrStr; if (a.family == 4 && a.address.size() == 4) { const uint8_t* b = (const uint8_t*)a.address.constData(); addrStr = QString("%1.%2.%3.%4").arg(b[0]).arg(b[1]).arg(b[2]).arg(b[3]); } else { addrStr = QString("ipv6:%1").arg(a.address.toHex().constData()); } addrList.append(QString("%1:%2").arg(addrStr).arg(a.port)); } setStatus(QString("Node: 0x%1 | %2") .arg(nodeId, 16, 16, QLatin1Char('0')) .arg(addrList.join(", "))); }); gui_bridge_set_connect_result_cb([](uint64_t nodeId, uint64_t channelId, int result) { JoinDialog* dlg = qobject_cast(QApplication::activeModalWidget()); if (dlg) dlg->onConnectResult(nodeId, channelId, result); }); if (!autoJoinLink.isEmpty()) { m_linkEdit->setText(autoJoinLink); QTimer::singleShot(0, this, &JoinDialog::onConnectClicked); } } JoinDialog::~JoinDialog() { gui_bridge_set_connect_result_cb(nullptr); } void JoinDialog::setStatus(const QString& text, bool isError) { if (isError) m_statusLabel->setStyleSheet("color: #D0021B; font-size: 12px; padding: 4px 0;"); else m_statusLabel->setStyleSheet("color: gray; font-size: 12px; padding: 4px 0;"); m_statusLabel->setText(text); } void JoinDialog::onLinkTextChanged() { m_decodeTimer->start(); } void JoinDialog::onConnectClicked() { if (m_connecting) return; QString link = m_linkEdit->text().trimmed(); if (link.isEmpty()) { setStatus("Enter an invite link.", true); return; } setStatus("Parsing link..."); InviteData d = decodeInviteLink(link); if (!d.error.isEmpty()) { setStatus("Invalid invite link: " + d.error, true); return; } if (d.pubkey.size() != 32) { setStatus(QString("Invalid invite link: pubkey is %1 bytes (need 32)").arg(d.pubkey.size()), true); return; } if (d.addrs.isEmpty()) { setStatus("No peer addresses in the invite link.", true); return; } uint64_t nodeId = sc_derive_node_id_from_pubkey((const uint8_t*)d.pubkey.constData()); GUI_INFO("join: decoded invite ch=%llu pubkey=%016llx... nodeId=0x%016llx addrs=%d", d.channelId, *(const uint64_t*)d.pubkey.constData(), nodeId, d.addrs.size()); m_pendingNodeId = nodeId; m_pendingChannelId = d.channelId; m_connecting = true; m_connectBtn->setEnabled(false); m_linkEdit->setReadOnly(true); setStatus("Connecting to peer..."); QByteArray addrsBuf; for (const auto& a : d.addrs) { addrsBuf.append(static_cast(a.family)); addrsBuf.append(static_cast(a.socketId)); addrsBuf.append(static_cast(a.proto)); addrsBuf.append(a.address); addrsBuf.append(static_cast((a.port >> 8) & 0xFF)); addrsBuf.append(static_cast(a.port & 0xFF)); } int addrCount = d.addrs.size(); const char* pass = d.password.isEmpty() ? NULL : d.password.toUtf8().constData(); chat_sync_connect_from_invite(d.channelId, nodeId, (const uint8_t*)d.pubkey.constData(), (const uint8_t*)addrsBuf.constData(), addrCount, addrsBuf.size(), pass); } void JoinDialog::onConnectResult(uint64_t nodeId, uint64_t channelId, int result) { if (!m_connecting || nodeId != m_pendingNodeId) return; if (result == 0) { setStatus("Connected!"); QString cid = QString::number(m_pendingChannelId); emit joined(cid); accept(); } else { m_connecting = false; m_connectBtn->setEnabled(true); m_linkEdit->setReadOnly(false); m_connectBtn->setFocus(); const char* errMsg = "Unknown error."; switch (result) { case -1: errMsg = "Channel not found."; break; case -3: errMsg = "Connection timed out."; break; case -4: errMsg = "Peer is unreachable."; break; case -6: errMsg = "Already connected to this peer."; break; default: break; } setStatus(QString("Connection failed: %1").arg(errMsg), true); } }