#include #include "inviteby.h" #include "invite_link.h" #include "debug_ui.h" #include "../transport/utun_node.h" #include "../transport/gui_bridge.h" #include "../db/db_manager.h" extern "C" { #include "chat/chat_core.h" #include "chat/chat_sync.h" #include "transport_layer/secure_channel.h" } #include #include #include #include #include #include #include #include InviteByLinkDialog::InviteByLinkDialog(const QString& channelId, UtunNode* node, QWidget* parent) : QDialog(parent), m_channelId(channelId), m_node(node) { setWindowTitle(QCoreApplication::translate("ChatGui", "Invite to channel %1").arg(channelId)); setMinimumWidth(420); setModal(true); auto *layout = new QVBoxLayout(this); layout->setSpacing(12); auto *title = new QLabel(QCoreApplication::translate("ChatGui", "Paste the invited node's utun:// link:"), this); title->setStyleSheet("font-weight: bold; font-size: 12px;"); layout->addWidget(title); m_inviteLinkEdit = new QLineEdit(this); m_inviteLinkEdit->setPlaceholderText("utun://..."); m_inviteLinkEdit->setStyleSheet( "QLineEdit { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 8px; font-size: 12px; background: palette(base); }"); layout->addWidget(m_inviteLinkEdit); m_inviteStatusLabel = new QLabel(this); m_inviteStatusLabel->setWordWrap(true); m_inviteStatusLabel->setStyleSheet("color: gray; font-size: 11px; padding: 2px 0;"); layout->addWidget(m_inviteStatusLabel); auto *btnLayout = new QHBoxLayout(); btnLayout->addStretch(); m_inviteSendBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Invite"), this); m_inviteSendBtn->setStyleSheet( "QPushButton { border: none; border-radius: 4px;" " padding: 5px 14px; font-size: 12px; font-weight: bold;" " background: #3390EC; color: white; }" "QPushButton:hover { background: #2B7ED4; }" "QPushButton:disabled { background: palette(mid); color: palette(midlight); }"); connect(m_inviteSendBtn, &QPushButton::clicked, this, &InviteByLinkDialog::onSendInvite); btnLayout->addWidget(m_inviteSendBtn); auto *closeBtn = new QPushButton("OK", this); connect(closeBtn, &QPushButton::clicked, this, &QDialog::accept); btnLayout->addWidget(closeBtn); layout->addLayout(btnLayout); connect(m_inviteLinkEdit, &QLineEdit::returnPressed, this, &InviteByLinkDialog::onSendInvite); connect(m_inviteLinkEdit, &QLineEdit::textChanged, this, &InviteByLinkDialog::onInviteLinkTextChanged); gui_bridge_set_connect_result_cb([](uint64_t nodeId, uint64_t channelId, int result) { InviteByLinkDialog* dlg = qobject_cast(QApplication::activeModalWidget()); if (dlg) dlg->onConnectResult(nodeId, channelId, result); }); } InviteByLinkDialog::~InviteByLinkDialog() { gui_bridge_set_connect_result_cb(nullptr); } void InviteByLinkDialog::onInviteLinkTextChanged() { QString link = m_inviteLinkEdit->text().trimmed(); if (link.isEmpty()) { m_inviteStatusLabel->setStyleSheet("color: gray; font-size: 11px; padding: 2px 0;"); m_inviteStatusLabel->clear(); return; } if (!link.startsWith(QLatin1String("utun://"))) { m_inviteStatusLabel->setStyleSheet("color: gray; font-size: 11px; padding: 2px 0;"); m_inviteStatusLabel->setText(QCoreApplication::translate("ChatGui", "Expected a utun:// link...")); return; } InviteData d = decodeInviteLink(link); if (!d.error.isEmpty()) { m_inviteStatusLabel->setStyleSheet("color: gray; font-size: 11px; padding: 2px 0;"); m_inviteStatusLabel->setText(QCoreApplication::translate("ChatGui", "Invalid link: ") + d.error); return; } uint64_t targetNodeId = sc_derive_node_id_from_pubkey((const uint8_t*)d.pubkey.constData()); uint64_t myNodeId = m_node ? m_node->nodeIdHex().toULongLong(nullptr, 16) : 0; QString pkHex = QString::fromLatin1(d.pubkey.toHex()); QString pkShort = pkHex.left(8) + ".." + pkHex.right(4); QString addrInfo; for (int i = 0; i < d.addrs.size() && i < 3; i++) { if (i > 0) addrInfo += ", "; if (d.addrs[i].family == 4) { const uint8_t* ab = (const uint8_t*)d.addrs[i].address.constData(); if (d.addrs[i].address.size() >= 4) addrInfo += QString("%1.%2.%3.%4:%5").arg(ab[0]).arg(ab[1]).arg(ab[2]).arg(ab[3]).arg(d.addrs[i].port); else addrInfo += QString("v4:?:%1").arg(d.addrs[i].port); } else { addrInfo += QString("v6:[%1..]:%2").arg( QString::fromLatin1(d.addrs[i].address.left(4).toHex())).arg(d.addrs[i].port); } } if (d.addrs.size() > 3) addrInfo += QCoreApplication::translate("ChatGui", " +%1 more").arg(d.addrs.size() - 3); QString diag = QCoreApplication::translate("ChatGui", "Link: node 0x%1 key %2, %3 addresses (%4)\nInvitation to channel: %5") .arg(targetNodeId, 16, 16, QChar('0')) .arg(pkShort) .arg(d.addrs.size()) .arg(addrInfo) .arg(m_channelId); if (myNodeId != 0 && myNodeId == targetNodeId) { m_inviteStatusLabel->setStyleSheet("color: #E67E22; font-size: 11px; padding: 2px 0;"); m_inviteStatusLabel->setText(diag + QCoreApplication::translate("ChatGui", " ⚠ This is your own link")); } else { m_inviteStatusLabel->setStyleSheet("color: gray; font-size: 11px; padding: 2px 0;"); m_inviteStatusLabel->setText(diag); } } void InviteByLinkDialog::setInviteStatus(const QString& text, bool isError) { if (isError) m_inviteStatusLabel->setStyleSheet("color: #D0021B; font-size: 11px; padding: 2px 0;"); else m_inviteStatusLabel->setStyleSheet("color: gray; font-size: 11px; padding: 2px 0;"); m_inviteStatusLabel->setText(text); } void InviteByLinkDialog::onSendInvite() { if (m_inviteSending) return; QString link = m_inviteLinkEdit->text().trimmed(); if (link.isEmpty()) { setInviteStatus(QCoreApplication::translate("ChatGui", "Paste the node's utun:// link."), true); return; } InviteData d = decodeInviteLink(link); if (!d.error.isEmpty()) { setInviteStatus(QCoreApplication::translate("ChatGui", "Invalid link: ") + d.error, true); return; } if (d.pubkey.size() != 32) { setInviteStatus(QCoreApplication::translate("ChatGui", "The public key must be 32 bytes."), true); return; } if (d.addrs.isEmpty()) { setInviteStatus(QCoreApplication::translate("ChatGui", "No addresses."), true); return; } uint64_t targetNodeId = sc_derive_node_id_from_pubkey((const uint8_t*)d.pubkey.constData()); { uint64_t myNodeId = m_node ? m_node->nodeIdHex().toULongLong(nullptr, 16) : 0; if (myNodeId != 0 && myNodeId == targetNodeId) { GUI_WARN("invite_by_link: self-invite rejected — link contains own pubkey, node=0x%016llx", (unsigned long long)myNodeId); setInviteStatus(QCoreApplication::translate("ChatGui", "Error: this is your own link (you cannot invite yourself)"), true); return; } } m_inviteSending = true; m_inviteSendBtn->setEnabled(false); m_inviteTargetNodeId = targetNodeId; GUI_INFO("invite_by_link: channel=%s target=0x%016llx addrs=%d", m_channelId.toUtf8().constData(), targetNodeId, d.addrs.size()); /* Save addresses to DB */ QByteArray addrsBuf = serializeInviteAddrs(d); chat_sync_invite_to_channel_with_addrs( gui_bridge_get_inst(), m_channelId.toUtf8().constData(), targetNodeId, (const uint8_t*)d.pubkey.constData(), (const uint8_t*)addrsBuf.constData(), d.addrs.size(), addrsBuf.size()); setInviteStatus(QCoreApplication::translate("ChatGui", "Invitation sent, waiting for a response...")); } void InviteByLinkDialog::onConnectResult(uint64_t nodeId, uint64_t channelId, int result) { if (!m_inviteSending || nodeId != m_inviteTargetNodeId) return; if (result == 0) { setInviteStatus(QCoreApplication::translate("ChatGui", "The node has joined the channel."), false); QTimer::singleShot(400, this, &QDialog::accept); } else { m_inviteSending = false; m_inviteSendBtn->setEnabled(true); m_inviteSendBtn->setFocus(); QString errMsg = QCoreApplication::translate("ChatGui", "Unknown error."); switch (result) { case -1: errMsg = QCoreApplication::translate("ChatGui", "Channel not found."); break; case 2: errMsg = QCoreApplication::translate("ChatGui", "Timed out — the node did not respond."); break; case -7: errMsg = QCoreApplication::translate("ChatGui", "Node not initialized."); break; default: break; } setInviteStatus(QCoreApplication::translate("ChatGui", "Error: %1").arg(errMsg), true); } }