chat: invite-ссылки с выбором достижимого узла + tcp_io фикс half-close FIN
- Общий модуль invite_build: сборка utun://-ссылок, авто-выбор лучшего достижимого узла, список кандидатов (вынесено из android jni_bridge) - chatgui: диалог invite с выбором узла (комбобокс), события INVITE_CANDIDATES / INVITE_LINK_READY через gui_bridge - headless/chatcli: invite <ch_id> [node_id], команда invite_nodes - tcp_io: error_cb отличает грациозный EPOLLHUP (SO_ERROR==0) от реальной ошибки — при приостановленном чтении FIN не роняет недослейные данные (фикс flaky half-close) - run_test.sh: half-close echo на выделенном порту 19190 + отдельный DNAT (был недостижим)
This commit is contained in:
1 parent
a17a85a383
commit
1345f44e10
23 files changed
+872
-539
No files matched your search
@@ -1,15 +1,29 @@
|
||||
#include "invitedialog.h"
|
||||
#include "qrcode_utils.h"
|
||||
#include "../db/db_manager.h"
|
||||
#include "../transport/gui_bridge.h"
|
||||
#include "debug_ui.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QComboBox>
|
||||
#include <QClipboard>
|
||||
#include <QApplication>
|
||||
#include <QSignalBlocker>
|
||||
#include <cstring>
|
||||
|
||||
ShareInviteDialog::ShareInviteDialog(const QString& link, QWidget* parent)
|
||||
: QDialog(parent), m_link(link)
|
||||
extern "C" {
|
||||
#include "../../../src/chat/invite_build.h"
|
||||
#include "../../../lib/mem.h"
|
||||
}
|
||||
|
||||
ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_channelId(channelId)
|
||||
, m_db(db)
|
||||
{
|
||||
setWindowTitle(QString::fromUtf8("Пригласительная ссылка"));
|
||||
setMinimumWidth(420);
|
||||
@@ -18,33 +32,152 @@ ShareInviteDialog::ShareInviteDialog(const QString& link, QWidget* parent)
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setSpacing(12);
|
||||
|
||||
auto *linkEdit = new QLineEdit(m_link, this);
|
||||
linkEdit->setReadOnly(true);
|
||||
linkEdit->setStyleSheet("padding: 6px; font-family: monospace;");
|
||||
layout->addWidget(linkEdit);
|
||||
auto *nodeLabel = new QLabel(QString::fromUtf8("Узел входа:"), this);
|
||||
nodeLabel->setStyleSheet("font-size: 12px; color: palette(window-text);");
|
||||
layout->addWidget(nodeLabel);
|
||||
|
||||
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); }");
|
||||
layout->addWidget(m_nodeCombo);
|
||||
|
||||
m_linkEdit = new QLineEdit(this);
|
||||
m_linkEdit->setReadOnly(true);
|
||||
m_linkEdit->setPlaceholderText(QString::fromUtf8("Генерация ссылки..."));
|
||||
m_linkEdit->setStyleSheet("padding: 6px; font-family: monospace;");
|
||||
layout->addWidget(m_linkEdit);
|
||||
|
||||
auto *btnLayout = new QHBoxLayout();
|
||||
auto *copyBtn = new QPushButton(QString::fromUtf8("Копировать"), this);
|
||||
connect(copyBtn, &QPushButton::clicked, this, [this, linkEdit]() {
|
||||
connect(copyBtn, &QPushButton::clicked, this, [this]() {
|
||||
QClipboard* cb = QApplication::clipboard();
|
||||
cb->setText(m_link, QClipboard::Clipboard);
|
||||
cb->setText(m_linkEdit->text(), QClipboard::Clipboard);
|
||||
if (cb->supportsSelection())
|
||||
cb->setText(m_link, QClipboard::Selection);
|
||||
linkEdit->setFocus();
|
||||
cb->setText(m_linkEdit->text(), QClipboard::Selection);
|
||||
m_linkEdit->setFocus();
|
||||
});
|
||||
btnLayout->addWidget(copyBtn);
|
||||
btnLayout->addStretch();
|
||||
layout->addLayout(btnLayout);
|
||||
|
||||
auto *qrLabel = new QLabel(this);
|
||||
QImage qrImage = generateQRCode(m_link);
|
||||
if (!qrImage.isNull())
|
||||
qrLabel->setPixmap(QPixmap::fromImage(qrImage).scaled(
|
||||
180, 180, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
qrLabel->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(qrLabel, 0, Qt::AlignCenter);
|
||||
m_qrLabel = new QLabel(this);
|
||||
m_qrLabel->setAlignment(Qt::AlignCenter);
|
||||
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<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &ShareInviteDialog::onNodeSelected);
|
||||
|
||||
/* колбэки результатов (по образцу JoinDialog) */
|
||||
gui_bridge_set_invite_link_ready_cb([](const char* chId, int chIdLen, const char* link) {
|
||||
ShareInviteDialog* dlg = qobject_cast<ShareInviteDialog*>(QApplication::activeModalWidget());
|
||||
if (dlg) dlg->onLinkReady(chId, chIdLen, link);
|
||||
});
|
||||
gui_bridge_set_invite_candidates_cb([](const char* chId, int chIdLen, const uint8_t* data, int len) {
|
||||
ShareInviteDialog* dlg = qobject_cast<ShareInviteDialog*>(QApplication::activeModalWidget());
|
||||
if (dlg) dlg->onCandidates(chId, chIdLen, data, len);
|
||||
});
|
||||
|
||||
/* запросить кандидатов; линк строится после их прихода */
|
||||
QByteArray chBytes = m_channelId.toUtf8();
|
||||
gui_bridge_post_uasync_fn(chat_invite_candidates_trampoline, u_strdup(chBytes.constData()));
|
||||
}
|
||||
|
||||
ShareInviteDialog::~ShareInviteDialog() {
|
||||
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));
|
||||
QByteArray chBytes = m_channelId.toUtf8();
|
||||
strncpy(req->ch_id, chBytes.constData(), sizeof(req->ch_id) - 1);
|
||||
req->target_node_id = targetNodeId;
|
||||
m_linkEdit->setText(QString());
|
||||
m_linkEdit->setPlaceholderText(QString::fromUtf8("Генерация ссылки..."));
|
||||
m_qrLabel->clear();
|
||||
gui_bridge_post_uasync_fn(chat_invite_build_link_trampoline, req);
|
||||
}
|
||||
|
||||
void ShareInviteDialog::setLink(const QString& link) {
|
||||
m_linkEdit->setText(link);
|
||||
QImage qrImage = generateQRCode(link);
|
||||
if (!qrImage.isNull())
|
||||
m_qrLabel->setPixmap(QPixmap::fromImage(qrImage).scaled(
|
||||
180, 180, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
|
||||
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(QString::fromUtf8("Не удалось определить узел (см. лог)"));
|
||||
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) + QString::fromUtf8(" (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 += QString::fromUtf8(" (self)");
|
||||
m_nodeCombo->addItem(label, QVariant::fromValue((qulonglong)nid));
|
||||
}
|
||||
|
||||
/* строим линк для авто-узла (сигнал заблокирован) */
|
||||
requestLink(autoId);
|
||||
}
|
||||
|
||||
void ShareInviteDialog::onLinkReady(const char* chId, int chIdLen, const char* link) {
|
||||
QString cid = QString::fromUtf8(chId, chIdLen);
|
||||
if (cid != m_channelId) return;
|
||||
if (!link || link[0] == '\0') {
|
||||
m_linkEdit->setText(QString());
|
||||
m_linkEdit->setPlaceholderText(QString::fromUtf8("Не удалось собрать ссылку (см. лог)"));
|
||||
m_qrLabel->clear();
|
||||
return;
|
||||
}
|
||||
setLink(QString::fromUtf8(link));
|
||||
}
|
||||
Reference in new issue
Block a user