- src/dm/: dm_core/dm_crypto/dm_mailbox — прямой обмен сообщениями (DM) - lib/soundtouch-master/: vendored SoundTouch (изменение темпа/высоты аудио) - doc/dm_arch.md, doc/chat_call_arch.md: архитектура DM и звонков - tests: test_dm.c, test_dm_e2e.c - chat: рефакторинг chat_* модулей, member_sync, invite_build - chatgui/chatgui-android: синхронизация с новым API чата - lib/u_async, debug_config, utun_instance: сопутствующие изменения
213 lines
8.8 KiB
C++
213 lines
8.8 KiB
C++
#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 <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QLineEdit>
|
|
#include <QPushButton>
|
|
#include <QLabel>
|
|
#include <QClipboard>
|
|
#include <QApplication>
|
|
#include <QTimer>
|
|
|
|
InviteByLinkDialog::InviteByLinkDialog(const QString& channelId, UtunNode* node, QWidget* parent)
|
|
: QDialog(parent), m_channelId(channelId), m_node(node)
|
|
{
|
|
setWindowTitle(QString::fromUtf8("Пригласить в канал %1").arg(channelId));
|
|
setMinimumWidth(420);
|
|
setModal(true);
|
|
|
|
auto *layout = new QVBoxLayout(this);
|
|
layout->setSpacing(12);
|
|
|
|
auto *title = new QLabel(QString::fromUtf8("Вставьте utun:// ссылку приглашаемого узла:"), 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(QString::fromUtf8("Пригласить"), 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<InviteByLinkDialog*>(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(QString::fromUtf8("Ожидается utun:// ссылка..."));
|
|
return;
|
|
}
|
|
|
|
InviteData d = decodeInviteLink(link);
|
|
if (!d.error.isEmpty()) {
|
|
m_inviteStatusLabel->setStyleSheet("color: gray; font-size: 11px; padding: 2px 0;");
|
|
m_inviteStatusLabel->setText(QString::fromUtf8("Неверная ссылка: ") + 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 += QString(" +%1 more").arg(d.addrs.size() - 3);
|
|
|
|
QString diag = QString::fromUtf8("Ссылка: узел 0x%1 ключ %2, %3 адр. (%4)\nПриглашение в канал: %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 + QString::fromUtf8(" ⚠ Это ваша собственная ссылка"));
|
|
} 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(QString::fromUtf8("Вставьте utun:// ссылку узла."), true);
|
|
return;
|
|
}
|
|
|
|
InviteData d = decodeInviteLink(link);
|
|
if (!d.error.isEmpty()) {
|
|
setInviteStatus(QString::fromUtf8("Неверная ссылка: ") + d.error, true);
|
|
return;
|
|
}
|
|
if (d.pubkey.size() != 32) {
|
|
setInviteStatus(QString::fromUtf8("Pubkey не 32 байта."), true);
|
|
return;
|
|
}
|
|
if (d.addrs.isEmpty()) {
|
|
setInviteStatus(QString::fromUtf8("Нет адресов."), 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(QString::fromUtf8("Ошибка: это ваша собственная ссылка (нельзя пригласить себя)"), 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(QString::fromUtf8("Приглашение отправлено, ожидание ответа..."));
|
|
}
|
|
|
|
void InviteByLinkDialog::onConnectResult(uint64_t nodeId, uint64_t channelId, int result) {
|
|
if (!m_inviteSending || nodeId != m_inviteTargetNodeId) return;
|
|
|
|
if (result == 0) {
|
|
setInviteStatus(QString::fromUtf8("Узел присоединился к каналу."), false);
|
|
QTimer::singleShot(400, this, &QDialog::accept);
|
|
} else {
|
|
m_inviteSending = false;
|
|
m_inviteSendBtn->setEnabled(true);
|
|
m_inviteSendBtn->setFocus();
|
|
|
|
const char* errMsg = "Неизвестная ошибка.";
|
|
switch (result) {
|
|
case -1: errMsg = "Канал не найден."; break;
|
|
case 2: errMsg = "Таймаут — узел не ответил."; break;
|
|
case -7: errMsg = "Узел не инициализирован."; break;
|
|
default: break;
|
|
}
|
|
setInviteStatus(QString::fromUtf8("Ошибка: %1").arg(QString::fromUtf8(errMsg)), true);
|
|
}
|
|
}
|