9 changed files with 259 additions and 248 deletions
@ -0,0 +1,189 @@ |
|||||||
|
#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); |
||||||
|
} |
||||||
|
|
||||||
|
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); |
||||||
|
|
||||||
|
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; |
||||||
|
for (const auto& a : d.addrs) { |
||||||
|
addrsBuf.append(static_cast<char>(a.family)); |
||||||
|
addrsBuf.append(static_cast<char>(a.socketId)); |
||||||
|
addrsBuf.append(static_cast<char>(a.proto)); |
||||||
|
addrsBuf.append(a.address); |
||||||
|
addrsBuf.append(static_cast<char>((a.port >> 8) & 0xFF)); |
||||||
|
addrsBuf.append(static_cast<char>(a.port & 0xFF)); |
||||||
|
} |
||||||
|
|
||||||
|
chat_sync_invite_to_channel(chat_core_get_inst(), m_channelId.toUtf8().constData(), targetNodeId); |
||||||
|
|
||||||
|
setInviteStatus(QString::fromUtf8("Приглашение отправлено. OK")); |
||||||
|
QTimer::singleShot(2000, this, [this]() { |
||||||
|
m_inviteSending = false; |
||||||
|
m_inviteSendBtn->setEnabled(true); |
||||||
|
}); |
||||||
|
} |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <QDialog> |
||||||
|
#include <QString> |
||||||
|
#include <QLineEdit> |
||||||
|
#include <QLabel> |
||||||
|
#include <QPushButton> |
||||||
|
|
||||||
|
class UtunNode; |
||||||
|
|
||||||
|
class InviteByLinkDialog : public QDialog { |
||||||
|
Q_OBJECT |
||||||
|
public: |
||||||
|
explicit InviteByLinkDialog(const QString& channelId, UtunNode* node, QWidget* parent = nullptr); |
||||||
|
|
||||||
|
private slots: |
||||||
|
void onInviteLinkTextChanged(); |
||||||
|
void onSendInvite(); |
||||||
|
|
||||||
|
private: |
||||||
|
void setInviteStatus(const QString& text, bool isError = false); |
||||||
|
|
||||||
|
QString m_channelId; |
||||||
|
UtunNode* m_node; |
||||||
|
QLineEdit* m_inviteLinkEdit = nullptr; |
||||||
|
QLabel* m_inviteStatusLabel = nullptr; |
||||||
|
QPushButton* m_inviteSendBtn = nullptr; |
||||||
|
bool m_inviteSending = false; |
||||||
|
uint64_t m_inviteTargetNodeId = 0; |
||||||
|
}; |
||||||
Loading…
Reference in new issue