Add local UDP discovery and invitation delivery for chat clients

This commit is contained in:
evgeny committed 2026-10-18 14:46:04 +02:00
1 parent aad295c9d0
commit 28826e050c
43 files changed
+1934 -43

No files matched your search

+103 -3
View File
@@ -14,14 +14,19 @@
#include <QClipboard>
#include <QApplication>
#include <QSignalBlocker>
#include <QPointer>
#include <QtEndian>
#include <cstring>
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<ShareInviteDialog> s_shareInviteDialog;
ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QWidget* parent)
: QDialog(parent)
, m_channelId(channelId)
@@ -30,6 +35,7 @@ ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QW
setWindowTitle(QCoreApplication::translate("ChatGui", "Invite link"));
setMinimumSize(420, 560);
setModal(true);
s_shareInviteDialog = this;
auto *layout = new QVBoxLayout(this);
layout->setSpacing(12);
@@ -38,7 +44,7 @@ ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QW
expiry->setWordWrap(true);
layout->addWidget(expiry);
auto *nodeLabel = new QLabel(QCoreApplication::translate("ChatGui", "Entry node:"), this);
auto *nodeLabel = new QLabel(QCoreApplication::translate("ChatGui", "Connect via"), this);
nodeLabel->setStyleSheet("font-size: 12px; color: palette(window-text);");
m_nodeCombo = new QComboBox(this);
@@ -72,6 +78,32 @@ ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QW
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<int>::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);
@@ -86,11 +118,11 @@ ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QW
/* колбэки результатов (по образцу JoinDialog) */
gui_bridge_set_invite_link_ready_cb([](const char* chId, int chIdLen, uint64_t requestId, const char* link) {
ShareInviteDialog* dlg = qobject_cast<ShareInviteDialog*>(QApplication::activeModalWidget());
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 = qobject_cast<ShareInviteDialog*>(QApplication::activeModalWidget());
ShareInviteDialog* dlg = s_shareInviteDialog;
if (dlg) dlg->onCandidates(chId, chIdLen, data, len);
});
@@ -103,6 +135,9 @@ ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QW
}
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);
}
@@ -127,6 +162,7 @@ void ShareInviteDialog::requestLink(uint64_t targetNodeId) {
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);
@@ -134,6 +170,7 @@ void ShareInviteDialog::requestLink(uint64_t targetNodeId) {
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(
@@ -195,9 +232,72 @@ void ShareInviteDialog::onLinkReady(const char* chId, int chIdLen, uint64_t requ
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<Device> devices;
int count = qFromBigEndian<quint16>(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<quint64>(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<quint64>(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();
}