#include #include "invitedialog.h" #include "qrcode_utils.h" #include "../db/db_manager.h" #include "../transport/gui_bridge.h" #include "debug_ui.h" #include #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include "../../../src/chat/invite_build.h" #include "../../../src/chat/chat_join.h" #include "../../../src/chat/chat_local.h" #include "../../../lib/mem.h" } static QPointer s_shareInviteDialog; ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QWidget* parent) : QDialog(parent) , m_channelId(channelId) , m_db(db) { setWindowTitle(QCoreApplication::translate("ChatGui", "Invite link")); setMinimumSize(420, 560); setModal(true); s_shareInviteDialog = this; auto *layout = new QVBoxLayout(this); layout->setSpacing(12); auto* expiry = new QLabel(QCoreApplication::translate("ChatGui", "The link expires %1 minutes after creation.").arg(JOIN_KEY_TTL_SECONDS / 60), this); expiry->setWordWrap(true); layout->addWidget(expiry); auto *nodeLabel = new QLabel(QCoreApplication::translate("ChatGui", "Connect via"), this); nodeLabel->setStyleSheet("font-size: 12px; color: palette(window-text);"); m_nodeCombo = new QComboBox(this); m_nodeCombo->setStyleSheet( "QComboBox { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 10px; font-size: 13px; background: palette(base); }"); auto *nodeRow = new QHBoxLayout(); nodeRow->setSpacing(8); nodeRow->addWidget(nodeLabel); nodeRow->addWidget(m_nodeCombo, 1); layout->addLayout(nodeRow); m_linkEdit = new QLineEdit(this); m_linkEdit->setReadOnly(true); m_linkEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Generating link...")); m_linkEdit->setStyleSheet("padding: 6px; font-family: monospace;"); auto *copyBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Copy"), this); connect(copyBtn, &QPushButton::clicked, this, [this]() { QClipboard* cb = QApplication::clipboard(); cb->setText(m_linkEdit->text(), QClipboard::Clipboard); if (cb->supportsSelection()) cb->setText(m_linkEdit->text(), QClipboard::Selection); m_linkEdit->setFocus(); }); auto *linkRow = new QHBoxLayout(); linkRow->setSpacing(8); linkRow->addWidget(m_linkEdit, 1); linkRow->addWidget(copyBtn); layout->addLayout(linkRow); m_localRow = new QWidget(this); auto* localLayout = new QVBoxLayout(m_localRow); localLayout->setContentsMargins(0, 0, 0, 0); localLayout->addWidget(new QLabel(QCoreApplication::translate("ChatGui", "Devices on the local network:"), m_localRow)); auto* deviceRow = new QHBoxLayout(); m_devices = new QComboBox(m_localRow); m_devices->setObjectName(QStringLiteral("localDevices")); m_sendLocal = new QPushButton(QCoreApplication::translate("ChatGui", "Send to device"), m_localRow); m_sendLocal->setObjectName(QStringLiteral("sendLocalInvite")); m_sendLocal->setEnabled(false); deviceRow->addWidget(m_devices, 1); deviceRow->addWidget(m_sendLocal); localLayout->addLayout(deviceRow); layout->addWidget(m_localRow); m_localRow->hide(); m_delivery = new QLabel(this); m_delivery->setWordWrap(true); layout->addWidget(m_delivery); connect(m_sendLocal, &QPushButton::clicked, this, &ShareInviteDialog::sendLocalInvite); connect(m_devices, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { updateLocalSend(); }); gui_bridge_set_local_devices_cb([](const uint8_t* data, int len) { if (s_shareInviteDialog) s_shareInviteDialog->onLocalDevices(data, len); }); gui_bridge_set_local_delivery_cb([](const uint8_t* data, int len) { if (s_shareInviteDialog) s_shareInviteDialog->onLocalDelivery(data, len); }); if (gui_bridge_is_uasync_ready() && gui_bridge_get_inst()) gui_bridge_post_uasync_fn(chat_local_peers_trampoline, gui_bridge_get_inst()); m_qrLabel = new QLabel(this); m_qrLabel->setAlignment(Qt::AlignCenter); m_qrLabel->setMinimumSize(180, 180); layout->addWidget(m_qrLabel, 0, Qt::AlignCenter); auto *closeBtn = new QPushButton("OK", this); connect(closeBtn, &QPushButton::clicked, this, &QDialog::accept); layout->addWidget(closeBtn, 0, Qt::AlignRight); connect(m_nodeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ShareInviteDialog::onNodeSelected); /* колбэки результатов (по образцу JoinDialog) */ gui_bridge_set_invite_link_ready_cb([](const char* chId, int chIdLen, uint64_t requestId, const char* link) { ShareInviteDialog* dlg = s_shareInviteDialog; if (dlg) dlg->onLinkReady(chId, chIdLen, requestId, link); }); gui_bridge_set_invite_candidates_cb([](const char* chId, int chIdLen, const uint8_t* data, int len) { ShareInviteDialog* dlg = s_shareInviteDialog; if (dlg) dlg->onCandidates(chId, chIdLen, data, len); }); /* запросить кандидатов; линк строится после их прихода */ struct chat_invite_cand_req* creq = (struct chat_invite_cand_req*)u_calloc(1, sizeof(*creq)); if (!creq) return; creq->inst = gui_bridge_get_inst(); strncpy(creq->ch_id, m_channelId.toUtf8().constData(), sizeof(creq->ch_id) - 1); gui_bridge_post_uasync_fn(chat_invite_candidates_trampoline, creq); } ShareInviteDialog::~ShareInviteDialog() { s_shareInviteDialog = nullptr; gui_bridge_set_local_devices_cb(nullptr); gui_bridge_set_local_delivery_cb(nullptr); gui_bridge_set_invite_link_ready_cb(nullptr); gui_bridge_set_invite_candidates_cb(nullptr); } void ShareInviteDialog::onNodeSelected(int index) { if (index < 0) return; qulonglong nodeId = m_nodeCombo->itemData(index).toULongLong(); requestLink((uint64_t)nodeId); } void ShareInviteDialog::requestLink(uint64_t targetNodeId) { struct chat_invite_build_req* req = (struct chat_invite_build_req*)u_malloc(sizeof(*req)); if (!req) { GUI_ERROR("invite: requestLink — alloc failed"); return; } memset(req, 0, sizeof(*req)); req->inst = gui_bridge_get_inst(); QByteArray chBytes = m_channelId.toUtf8(); strncpy(req->ch_id, chBytes.constData(), sizeof(req->ch_id) - 1); static uint64_t nextRequestId = 0; req->request_id = m_requestId = ++nextRequestId; req->target_node_id = targetNodeId; m_linkEdit->setText(QString()); updateLocalSend(); m_linkEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Generating link...")); m_qrLabel->clear(); gui_bridge_post_uasync_fn(chat_invite_build_link_trampoline, req); } void ShareInviteDialog::setLink(const QString& link) { m_linkEdit->setText(link); updateLocalSend(); QImage qrImage = generateQRCode(link); if (!qrImage.isNull()) m_qrLabel->setPixmap(QPixmap::fromImage(qrImage).scaled( 180, 180, Qt::KeepAspectRatio, Qt::SmoothTransformation)); adjustSize(); } void ShareInviteDialog::onCandidates(const char* chId, int chIdLen, const uint8_t* data, int len) { QString cid = QString::fromUtf8(chId, chIdLen); if (cid != m_channelId) return; if (!data || len < 10) return; uint64_t autoId = 0; memcpy(&autoId, data, 8); uint16_t count = 0; memcpy(&count, data + 8, 2); if (autoId == 0) { m_linkEdit->setText(QString()); m_linkEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Failed to identify the node (see log)")); return; } auto nodeLabel = [this](uint64_t nid) { QString name = m_db ? m_db->getDisplayName(nid) : QString(); if (name.isEmpty()) name = QString("0x%1").arg(nid, 16, 16, QChar('0')); return name; }; /* блокируем сигналы, чтобы не перезапрашивать линк при добавлении пунктов */ QSignalBlocker blocker(m_nodeCombo); m_nodeCombo->clear(); /* пункт 0 — узел, найденный авто-выбором */ m_nodeCombo->addItem(nodeLabel(autoId) + QCoreApplication::translate("ChatGui", " (auto)"), QVariant::fromValue((qulonglong)autoId)); /* остальные узлы (пропуская авто), свой помечаем (self) */ int off = 10; for (int i = 0; i < (int)count && off + 8 <= len; i++) { uint64_t nid = 0; memcpy(&nid, data + off, 8); off += 8; if (nid == autoId) continue; QString label = nodeLabel(nid); if (m_db && nid == m_db->myNodeId()) label += QCoreApplication::translate("ChatGui", " (self)"); m_nodeCombo->addItem(label, QVariant::fromValue((qulonglong)nid)); } /* строим линк для авто-узла (сигнал заблокирован) */ requestLink(autoId); } void ShareInviteDialog::onLinkReady(const char* chId, int chIdLen, uint64_t requestId, const char* link) { if (requestId != m_requestId) return; QString cid = QString::fromUtf8(chId, chIdLen); if (cid != m_channelId) return; if (!link || link[0] == '\0') { m_linkEdit->setText(QString()); updateLocalSend(); m_linkEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Failed to build the link (see log)")); m_qrLabel->clear(); return; } setLink(QString::fromUtf8(link)); } void ShareInviteDialog::updateLocalSend() { m_sendLocal->setEnabled(!m_localRequest && m_devices->currentIndex() >= 0 && !m_linkEdit->text().isEmpty()); } void ShareInviteDialog::onLocalDevices(const uint8_t* data, int len) { if (len < 2) { GUI_ERROR("discovery: invalid device list bytes=%d", len); return; } struct Device { quint64 id; QString name; }; QList devices; int count = qFromBigEndian(data), offset = 2; for (int i = 0; i < count; i++) { if (offset + 9 > len || offset + 9 + data[offset + 8] > len) { GUI_ERROR("discovery: truncated device list bytes=%d offset=%d", len, offset); return; } int nameLen = data[offset + 8]; devices.append({qFromBigEndian(data + offset), QString::fromUtf8((const char*)data + offset + 9, nameLen)}); offset += 9 + nameLen; } if (offset != len) { GUI_ERROR("discovery: trailing device list bytes=%d", len - offset); return; } auto selected = m_devices->currentData(); QSignalBlocker blocker(m_devices); m_devices->clear(); for (const auto& device : devices) { const QString suffix = QString::number(device.id, 16).right(6); m_devices->addItem(device.name + QStringLiteral(" [%1]").arg(suffix), QVariant::fromValue(device.id)); } int index = m_devices->findData(selected); if (index >= 0) m_devices->setCurrentIndex(index); m_localRow->setVisible(!devices.isEmpty()); updateLocalSend(); } void ShareInviteDialog::sendLocalInvite() { QByteArray link = m_linkEdit->text().toUtf8(); if (m_localRequest || m_devices->currentIndex() < 0 || link.isEmpty()) return; if (!gui_bridge_is_uasync_ready() || !gui_bridge_get_inst()) { GUI_ERROR("discovery: core stopped before local send"); m_delivery->setText(QCoreApplication::translate("ChatGui", "Could not deliver invitation")); return; } if (link.size() > LOCAL_TRANSPORT_DATA_MAX) { GUI_ERROR("discovery: invitation too long bytes=%d", (int)link.size()); m_delivery->setText(QCoreApplication::translate("ChatGui", "Could not deliver invitation")); return; } auto* req = (struct chat_local_send_request*)u_calloc(1, sizeof(struct chat_local_send_request)); if (!req) { GUI_ERROR("discovery: send request allocation failed"); return; } static uint64_t nextRequest = 0; req->inst = gui_bridge_get_inst(); req->peer_id = m_devices->currentData().toULongLong(); req->request_id = m_localRequest = ++nextRequest; memcpy(req->link, link.constData(), (size_t)link.size()); m_delivery->setText(QCoreApplication::translate("ChatGui", "Sending invitation…")); updateLocalSend(); gui_bridge_post_uasync_fn(chat_local_send_trampoline, req); } void ShareInviteDialog::onLocalDelivery(const uint8_t* data, int len) { if (len != 17 || data[16] > LOCAL_DELIVERY_FORBIDDEN) { GUI_ERROR("discovery: invalid delivery response bytes=%d", len); return; } if (!m_localRequest || qFromBigEndian(data) != m_localRequest) return; m_localRequest = 0; if (data[16] == LOCAL_DELIVERY_FORBIDDEN) m_delivery->setText(QCoreApplication::translate("ChatGui", "Receiving invitations is forbidden. Enable it in the recipient device's settings.")); else m_delivery->setText(QCoreApplication::translate("ChatGui", data[16] == LOCAL_DELIVERY_RECEIVED ? "Invitation delivered" : "Could not deliver invitation")); updateLocalSend(); }