Browse Source

etc/rtt: UPSERT node_update_verified; gui: account delegate (rtt+nat subline), context menu member props (admin/supernode), cleanOrphanedPeers, refreshRtt timer 2s

topo_upd
Evgeny 2 months ago
parent
commit
090acc1f00
  1. 2
      src/routing_layer/topo_node.c
  2. 115
      src/routing_layer/topo_node_sqlite.c
  3. 3
      src/routing_layer/topo_node_sqlite.h
  4. 2
      tools/chatgui/CMakeLists.txt
  5. 13
      tools/chatgui/db/db_manager.cpp
  6. 1
      tools/chatgui/db/db_manager.h
  7. 60
      tools/chatgui/src/accountdelegate.cpp
  8. 14
      tools/chatgui/src/accountdelegate.h
  9. 29
      tools/chatgui/src/accountlist.cpp
  10. 4
      tools/chatgui/src/accountlist.h
  11. 83
      tools/chatgui/src/memberlistmodel.cpp
  12. 3
      tools/chatgui/src/memberlistmodel.h
  13. 168
      tools/chatgui/src/memberpropsdialog.cpp
  14. 27
      tools/chatgui/src/memberpropsdialog.h

2
src/routing_layer/topo_node.c

@ -307,7 +307,7 @@ void topo_node_ping_update_rtt(struct TOPO_GROUPS* groups, uint64_t node_id, uin
DEBUG_INFO(DEBUG_CATEGORY_BGP, "ping_update_rtt: node=%016llx rtt=%u (%llu) group=%016llx",
(unsigned long long)node_id, (unsigned)rtt, (unsigned long long)now, (unsigned long long)g->group_id);
if (groups->instance && groups->instance->topo_sqlite_db)
topo_node_sqlite_nodeinfo_updated(groups->instance->topo_sqlite_db, node_id);
topo_node_sqlite_update_rtt(groups->instance->topo_sqlite_db, node_id, rtt);
}
ge = ge->next;
}

115
src/routing_layer/topo_node_sqlite.c

@ -2,6 +2,7 @@
#include "topo_node.h"
#include "etcp_connections.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include <string.h>
#include <stdio.h>
@ -248,30 +249,39 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
if (!db || !channel_id || !name || !x25519_pub || !ed25519_pub || !signature) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[512];
snprintf(sql, sizeof(sql),
"CREATE TABLE IF NOT EXISTS \"%s\" (" // таблица мемберов канала
const char* ddl_tmpl =
"CREATE TABLE IF NOT EXISTS \"%s\" ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" node_id INTEGER NOT NULL UNIQUE," // ид мембера группы
" x25519_pubkey BLOB NOT NULL," // ключ мембера для шифрования
" ed25519_pubkey BLOB NOT NULL," // ключ мембера для подписи
" join_sig BLOB NOT NULL," // подпись мембера неизменяемая (при присоединении) сh_x25519_pubkey(32) || ch_ed25519_pubkey(32) || node_id(8 LE) || node_x25519_pubkey(32) || join_ts(8 LE)
" join_ts INTEGER NOT NULL," // время присоединения
" update_sig BLOB," // подпись мембера изменяемая join_sig(64) || update_ts(8) || userinfo (NUL-terminated)
" update_ts INTEGER DEFAULT 0,"// время подписи
" userinfo TEXT NOT NULL DEFAULT ''," // имя/метаданные узла в канале (json_flat style)
" creator_sig BLOB," // валидация мембера админом канала
" adm_tags TEXT," // тэги которые может менять админ (json_flat style)
" adm_tags_sig BLOB," // подпись тэгов adm_tags (может быть подписана ключом суперадмина или админов - проверяем перебором)
" node_type INTEGER NOT NULL DEFAULT 0," // ENUM(0-'PRIV',1-'NAT',2-'EIM-NAT',3-'PUBLIC',4-SUPERNODE) тип ноды - private (только нода подключается), nat (односторонние подключения), eim-nat (доступны подключения но надо запрашивать ip), public - на прямых ip (но может быть недоступна в чебурнетах), supernode - на прямых ip, рекомендуемая для подключения
" node_RTT INTEGER" // задержка в ms
")", peers_tbl);
" node_id INTEGER NOT NULL UNIQUE,"
" x25519_pubkey BLOB NOT NULL,"
" ed25519_pubkey BLOB NOT NULL,"
" join_sig BLOB NOT NULL,"
" join_ts INTEGER NOT NULL,"
" update_sig BLOB,"
" update_ts INTEGER DEFAULT 0,"
" userinfo TEXT NOT NULL DEFAULT '',"
" creator_sig BLOB,"
" adm_tags TEXT,"
" adm_tags_sig BLOB,"
" node_type INTEGER NOT NULL DEFAULT 0,"
" node_RTT INTEGER"
")";
int ddl_sz = snprintf(NULL, 0, ddl_tmpl, peers_tbl);
char* ddl = u_malloc(ddl_sz + 1);
if (ddl) {
snprintf(ddl, ddl_sz + 1, ddl_tmpl, peers_tbl);
char* err = NULL;
sqlite3_exec(db, sql, NULL, NULL, &err);
sqlite3_exec(db, ddl, NULL, NULL, &err);
if (err) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "topo_node_sqlite_channel_put peers: %s", err); sqlite3_free(err); }
snprintf(sql, sizeof(sql),
u_free(ddl);
}
char idx_sql[128];
snprintf(idx_sql, sizeof(idx_sql),
"CREATE INDEX IF NOT EXISTS \"idx_%s_type_rtt\" ON \"%s\" (node_type, node_RTT)", peers_tbl, peers_tbl);
sqlite3_exec(db, sql, NULL, NULL, NULL);
sqlite3_exec(db, idx_sql, NULL, NULL, NULL);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
@ -324,17 +334,24 @@ int topo_node_sqlite_member_put(sqlite3* db, const char* channel_id, uint64_t no
int topo_node_sqlite_node_update_verified(sqlite3* db, uint64_t node_id,
const char* name, const uint8_t* x25519, const uint8_t* ed25519, uint64_t update_ts) {
if (!db || !name || !x25519 || !ed25519) return -1;
if (!db || !x25519 || !ed25519) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
"UPDATE nodes SET name=?, x25519_pubkey=?, ed25519_pubkey=?, update_ts=?"
" WHERE node_id=? AND (update_ts IS NULL OR update_ts < ?)", -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 2, x25519, 32, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 3, ed25519, 32, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 4, (sqlite3_int64)update_ts);
sqlite3_bind_int64(stmt, 5, (sqlite3_int64)node_id);
sqlite3_bind_int64(stmt, 6, (sqlite3_int64)update_ts);
"INSERT INTO nodes(node_id, name, x25519_pubkey, ed25519_pubkey, update_ts)"
" VALUES(?,?,?,?,?)"
" ON CONFLICT(node_id) DO UPDATE SET"
" name=CASE WHEN excluded.name IS NOT NULL AND excluded.name != ''"
" THEN excluded.name ELSE nodes.name END,"
" x25519_pubkey=excluded.x25519_pubkey,"
" ed25519_pubkey=excluded.ed25519_pubkey,"
" update_ts=CASE WHEN nodes.update_ts IS NULL OR excluded.update_ts > nodes.update_ts"
" THEN excluded.update_ts ELSE nodes.update_ts END",
-1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
sqlite3_bind_text(stmt, 2, name && name[0] ? name : "", -1, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 3, x25519, 32, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 4, ed25519, 32, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 5, (sqlite3_int64)update_ts);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
return 0;
@ -584,3 +601,43 @@ int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id) {
(unsigned long long)node_id, best_at, base_type);
return 0;
}
void topo_node_sqlite_update_rtt(sqlite3* db, uint64_t node_id, uint16_t rtt) {
if (!db) return;
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"UPDATE node_addresses SET rtt=? WHERE node_id=?",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int(s, 1, (int)rtt);
sqlite3_bind_int64(s, 2, (sqlite3_int64)node_id);
sqlite3_step(s);
sqlite3_finalize(s);
}
}
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'",
-1, &s, NULL) != SQLITE_OK) return;
char sql[256];
while (sqlite3_step(s) == SQLITE_ROW) {
const char* tbl = (const char*)sqlite3_column_text(s, 0);
snprintf(sql, sizeof(sql),
"UPDATE \"%s\" SET node_RTT=? WHERE node_id=?", tbl);
sqlite3_stmt* u = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) {
sqlite3_bind_int(u, 1, (int)rtt);
sqlite3_bind_int64(u, 2, (sqlite3_int64)node_id);
sqlite3_step(u); sqlite3_finalize(u);
}
}
sqlite3_finalize(s);
}
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "sqlite_update_rtt: node=%016llx rtt=%u",
(unsigned long long)node_id, (unsigned)rtt);
}

3
src/routing_layer/topo_node_sqlite.h

@ -52,4 +52,7 @@ int topo_node_sqlite_get_ed25519_pubkey(sqlite3* db, uint64_t node_id, uint8_t p
/** Пересчитывает node_type во всех peers_* таблицах на основе addr_type из node_addresses */
int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id);
/** Обновляет RTT узла: node_addresses.rtt и все peers_*.node_RTT */
void topo_node_sqlite_update_rtt(sqlite3* db, uint64_t node_id, uint16_t rtt);
#endif

2
tools/chatgui/CMakeLists.txt

@ -55,6 +55,8 @@ add_executable(chatgui
src/messagedelegate.cpp
src/messagelist.cpp
src/accountlist.cpp
src/accountdelegate.cpp
src/memberpropsdialog.cpp
src/memberlistmodel.cpp
src/emoji.cpp
src/emojipanel.cpp

13
tools/chatgui/db/db_manager.cpp

@ -197,6 +197,19 @@ QList<ChannelMember> DbManager::getChannelMembers(const QString& chId, int offse
return list;
}
void DbManager::cleanOrphanedPeers(const QString& chId) const {
if (!m_db) return;
QString sql = QStringLiteral(
"DELETE FROM \"%1\" WHERE node_id NOT IN (SELECT node_id FROM nodes)"
).arg(peersTableName(chId));
char* err = nullptr;
int rc = sqlite3_exec(m_db, sql.toUtf8().constData(), nullptr, nullptr, &err);
if (rc != SQLITE_OK) {
qWarning("cleanOrphanedPeers: %s", err ? err : "?");
sqlite3_free(err);
}
}
QString DbManager::getDisplayName(quint64 nodeId) const {
sqlite3_stmt* st = prepareOrNull(
"SELECT display_name FROM accounts WHERE node_id=?");

1
tools/chatgui/db/db_manager.h

@ -88,6 +88,7 @@ public:
/* ── Участники канала (UI) ── */
int getChannelMemberCount(const QString& chId) const;
QList<ChannelMember> getChannelMembers(const QString& chId, int offset, int limit) const;
void cleanOrphanedPeers(const QString& chId) const;
QString getDisplayName(quint64 nodeId) const;
/* ── Узлы ── */

60
tools/chatgui/src/accountdelegate.cpp

@ -0,0 +1,60 @@
#include "accountdelegate.h"
#include "memberlistmodel.h"
#include <QPainter>
#include <QFont>
AccountDelegate::AccountDelegate(QObject *parent)
: QStyledItemDelegate(parent) {}
void AccountDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const {
painter->save();
if (option.state & QStyle::State_Selected)
painter->fillRect(option.rect, option.palette.highlight());
else if (option.state & QStyle::State_MouseOver)
painter->fillRect(option.rect, QColor(240, 240, 240));
const int margin = 4;
const int iconSz = 24;
QIcon icon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole));
if (!icon.isNull())
icon.paint(painter, option.rect.left() + margin, option.rect.top() + (option.rect.height() - iconSz) / 2, iconSz, iconSz);
const int x = option.rect.left() + iconSz + margin * 3;
const int w = option.rect.right() - x - margin;
const int yMid = option.rect.top() + option.rect.height() / 2;
QFont nameFont = painter->font();
nameFont.setBold(true);
nameFont.setPixelSize(13);
painter->setFont(nameFont);
QColor textColor = option.palette.color(QPalette::Text);
if (option.state & QStyle::State_Selected) textColor = option.palette.color(QPalette::HighlightedText);
painter->setPen(textColor);
QString name = index.data(Qt::DisplayRole).toString();
painter->drawText(QRect(x, option.rect.top() + 2, w, yMid - option.rect.top() - 2),
Qt::AlignLeft | Qt::AlignBottom, name);
QFont subFont = painter->font();
subFont.setBold(false);
subFont.setPixelSize(10);
painter->setFont(subFont);
painter->setPen(QColor(130, 130, 130));
QString sub = index.data(Qt::UserRole + 1).toString();
if (!sub.isEmpty())
painter->drawText(QRect(x, yMid + 2, w, option.rect.bottom() - yMid - 2),
Qt::AlignLeft | Qt::AlignTop, sub);
painter->restore();
}
QSize AccountDelegate::sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const {
(void)option; (void)index;
return QSize(160, 36);
}

14
tools/chatgui/src/accountdelegate.h

@ -0,0 +1,14 @@
#pragma once
#include <QStyledItemDelegate>
class AccountDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
explicit AccountDelegate(QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
};

29
tools/chatgui/src/accountlist.cpp

@ -1,5 +1,7 @@
#include "accountlist.h"
#include "accountdelegate.h"
#include "memberlistmodel.h"
#include "memberpropsdialog.h"
#include "../db/db_manager.h"
#include <QVBoxLayout>
#include <QLabel>
@ -52,11 +54,13 @@ AccountList::AccountList(DbManager* db, QWidget *parent)
layout->addWidget(header);
m_listView->setModel(m_model);
m_listView->setItemDelegate(new AccountDelegate(m_listView));
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_listView->setSelectionMode(QAbstractItemView::SingleSelection);
m_listView->setIconSize(QSize(24, 24));
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
m_listView->setContextMenuPolicy(Qt::CustomContextMenu);
layout->addWidget(m_listView, 3);
m_detailText = new QPlainTextEdit(this);
@ -69,6 +73,13 @@ AccountList::AccountList(DbManager* db, QWidget *parent)
layout->addWidget(m_detailText, 2);
connect(m_listView, &QListView::clicked, this, &AccountList::onMemberClicked);
m_refreshTimer = new QTimer(this);
m_refreshTimer->start(2000);
connect(m_refreshTimer, &QTimer::timeout, m_model, &MemberListModel::refreshRtt);
connect(m_listView, &QWidget::customContextMenuRequested,
this, &AccountList::onMemberContextMenu);
}
void AccountList::setChannel(const QString& channelId) {
@ -153,3 +164,21 @@ void AccountList::showMemberDetail(quint64 nodeId) {
m_detailText->setPlainText(text);
}
void AccountList::onMemberContextMenu(const QPoint& pos) {
QModelIndex index = m_listView->indexAt(pos);
if (!index.isValid()) return;
quint64 nodeId = m_model->nodeIdAt(index.row());
if (!nodeId) return;
QString name = m_model->data(index, Qt::DisplayRole).toString();
QMenu menu(m_listView);
QAction* editAction = menu.addAction("Edit");
QAction* chosen = menu.exec(m_listView->viewport()->mapToGlobal(pos));
if (chosen != editAction) return;
MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, this);
dlg.exec();
}

4
tools/chatgui/src/accountlist.h

@ -3,6 +3,8 @@
#include <QWidget>
#include <QListView>
#include <QPlainTextEdit>
#include <QTimer>
#include <QMenu>
class MemberListModel;
class DbManager;
@ -17,6 +19,7 @@ public:
private slots:
void onMemberClicked(const QModelIndex& index);
void onMemberContextMenu(const QPoint& pos);
private:
void showMemberDetail(quint64 nodeId);
@ -25,5 +28,6 @@ private:
MemberListModel *m_model;
DbManager *m_db;
QPlainTextEdit *m_detailText;
QTimer *m_refreshTimer;
QString m_channelId;
};

83
tools/chatgui/src/memberlistmodel.cpp

@ -20,6 +20,17 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const {
if (it != m_cache.end()) {
if (role == Qt::DisplayRole) return it->name;
if (role == Qt::DecorationRole) return QVariant::fromValue(QIcon(it->avatar));
if (role == Qt::UserRole + 1) {
QString sub;
if (it->rtt >= 0) sub += QString("rtt=%1ms").arg(it->rtt);
if (it->natType >= 0) {
static const char* nat[] = {"NET","DIR","EIM","STR"};
const char* nt = (it->natType <= 3) ? nat[it->natType] : "?";
if (!sub.isEmpty()) sub += " ";
sub += nt;
}
return sub;
}
if (role == Qt::ToolTipRole)
return QString(it->online ? "online" : "offline") + " | 0x" +
QString("%1").arg(it->nodeId, 16, 16, QChar('0'));
@ -36,6 +47,7 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const {
void MemberListModel::setChannel(const QString& channelId) {
beginResetModel();
m_channelId = channelId;
m_db->cleanOrphanedPeers(channelId);
m_totalCount = m_db ? m_db->getChannelMemberCount(channelId) : 0;
m_cache.clear();
m_pendingFrom = -1;
@ -84,12 +96,83 @@ void MemberListModel::loadBatch(int from, int count) {
QColor c = members[i].online ? QColor("#4CAF50") : QColor("#9E9E9E");
item.avatar = makeAvatar(c, item.name[0]);
}
// batch-загрузка RTT и NAT типа из node_addresses
QStringList idList;
for (int i = 0; i < members.size(); i++)
idList.append(QString::number(members[i].nodeId));
QString ids = idList.isEmpty() ? QString("0") : idList.join(",");
QByteArray sql = QString(
"SELECT node_id, MIN(rtt), MIN(addr_type) FROM node_addresses"
" WHERE node_id IN (%1) AND rtt > 0 GROUP BY node_id"
).arg(ids).toUtf8();
sqlite3_stmt* na = nullptr;
if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &na, nullptr) == SQLITE_OK) {
while (sqlite3_step(na) == SQLITE_ROW) {
quint64 nid = (quint64)sqlite3_column_int64(na, 0);
int rttVal = sqlite3_column_int(na, 1);
int at = sqlite3_column_int(na, 2);
for (int i = 0; i < members.size(); i++) {
if (members[i].nodeId == nid) {
int row = from + i;
auto it = m_cache.find(row);
if (it != m_cache.end()) {
it->rtt = rttVal;
it->natType = at;
}
break;
}
}
}
sqlite3_finalize(na);
}
evictDistant(from + BATCH / 2);
int last = from + (int)members.size() - 1;
if (last >= from) emit dataChanged(index(from), index(last));
}
void MemberListModel::refreshRtt() {
if (m_cache.isEmpty() || !m_db || !m_db->m_db) return;
QStringList idList;
for (auto it = m_cache.begin(); it != m_cache.end(); ++it)
idList.append(QString::number(it->nodeId));
if (idList.isEmpty()) return;
QString ids = idList.join(",");
QByteArray sql = QString(
"SELECT node_id, MIN(rtt), MIN(addr_type) FROM node_addresses"
" WHERE node_id IN (%1) AND rtt > 0 GROUP BY node_id"
).arg(ids).toUtf8();
sqlite3_stmt* na = nullptr;
if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &na, nullptr) != SQLITE_OK) return;
QList<int> changed;
while (sqlite3_step(na) == SQLITE_ROW) {
quint64 nid = (quint64)sqlite3_column_int64(na, 0);
int rttVal = sqlite3_column_int(na, 1);
int at = sqlite3_column_int(na, 2);
for (auto it = m_cache.begin(); it != m_cache.end(); ++it) {
if (it->nodeId == nid) {
if (it->rtt != rttVal || it->natType != at) {
it->rtt = rttVal;
it->natType = at;
changed.append(it.key());
}
break;
}
}
}
sqlite3_finalize(na);
for (int row : changed) emit dataChanged(index(row), index(row), {Qt::UserRole + 1});
}
void MemberListModel::evictDistant(int centerRow) {
if (m_cache.size() <= MAX_CACHE) return;
int half = MAX_CACHE / 2;

3
tools/chatgui/src/memberlistmodel.h

@ -15,6 +15,8 @@ struct MemberListItem {
bool online = false;
QPixmap avatar;
QString name;
int rtt = -1; // ms (min из node_addresses, -1 если нет)
int natType = -1; // addr_type: 0=NET, 1=DIR, 2=EIM, 3=STR
};
class MemberListModel : public QAbstractListModel {
@ -30,6 +32,7 @@ public:
void setChannel(const QString& channelId);
void refresh();
void refreshRtt();
quint64 nodeIdAt(int row) const;
private slots:

168
tools/chatgui/src/memberpropsdialog.cpp

@ -0,0 +1,168 @@
#include "memberpropsdialog.h"
#include "../db/db_manager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QMessageBox>
#include <openssl/evp.h>
MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
quint64 nodeId, const QString& memberName,
QWidget* parent)
: QDialog(parent)
, m_db(db)
, m_channelId(channelId)
, m_nodeId(nodeId)
{
setWindowTitle("Member Properties");
setMinimumWidth(340);
setModal(true);
// читаем ed25519_privkey канала
auto chs = m_db->getChannels();
for (const auto& ch : chs) {
if (ch.channelId == channelId) {
m_edPrivkey = ch.ed25519_privkey;
break;
}
}
bool haveKey = (m_edPrivkey.size() == 32);
// читаем текущий adm_tags
bool isAdmin = false, isSuper = false;
if (m_db->m_db) {
QString tbl = m_db->peersTableName(channelId);
QByteArray sql = QString(
"SELECT adm_tags FROM \"%1\" WHERE node_id=?"
).arg(tbl).toUtf8();
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &st, nullptr) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(st, 0);
if (tags) {
if (strstr(tags, "admin=yes")) isAdmin = true;
if (strstr(tags, "supernode=yes")) isSuper = true;
}
}
sqlite3_finalize(st);
}
// страховка: добавить колонки если нет
QByteArray alt = QString(
"ALTER TABLE \"%1\" ADD COLUMN adm_tags TEXT"
).arg(tbl).toUtf8();
sqlite3_exec(m_db->m_db, alt.constData(), nullptr, nullptr, nullptr);
alt = QString(
"ALTER TABLE \"%1\" ADD COLUMN adm_tags_sig BLOB"
).arg(tbl).toUtf8();
sqlite3_exec(m_db->m_db, alt.constData(), nullptr, nullptr, nullptr);
}
auto* layout = new QVBoxLayout(this);
layout->setSpacing(12);
layout->setContentsMargins(20, 16, 20, 16);
auto* title = new QLabel(
QString("Node: 0x%1").arg(nodeId, 16, 16, QChar('0')) +
(memberName.isEmpty() ? "" : QString(" (%1)").arg(memberName)),
this);
title->setStyleSheet("font-weight: bold; font-size: 13px;");
layout->addWidget(title);
m_adminCb = new QCheckBox("Admin", this);
m_adminCb->setChecked(isAdmin);
m_adminCb->setEnabled(haveKey);
layout->addWidget(m_adminCb);
m_superCb = new QCheckBox("Supernode", this);
m_superCb->setChecked(isSuper);
m_superCb->setEnabled(haveKey);
layout->addWidget(m_superCb);
if (!haveKey) {
auto* hint = new QLabel("(requires channel private key)", this);
hint->setStyleSheet("color: gray; font-size: 11px;");
layout->addWidget(hint);
}
layout->addStretch();
auto* btnLayout = new QHBoxLayout();
btnLayout->addStretch();
auto* cancelBtn = new QPushButton("Cancel", this);
cancelBtn->setFixedWidth(90);
cancelBtn->setStyleSheet(
"QPushButton { border: 1px solid palette(mid); border-radius: 4px;"
" padding: 6px 16px; font-size: 13px; background: palette(button); }"
"QPushButton:hover { background: palette(light); }");
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
btnLayout->addWidget(cancelBtn);
auto* saveBtn = new QPushButton("Save", this);
saveBtn->setFixedWidth(90);
saveBtn->setDefault(true);
saveBtn->setEnabled(haveKey);
saveBtn->setStyleSheet(
"QPushButton { border: none; border-radius: 4px;"
" padding: 6px 16px; font-size: 13px; font-weight: bold;"
" background: #3390EC; color: white; }"
"QPushButton:hover { background: #2B7ED4; }"
"QPushButton:disabled { background: palette(mid); color: palette(midlight); }");
connect(saveBtn, &QPushButton::clicked, this, &MemberPropsDialog::onSave);
btnLayout->addWidget(saveBtn);
layout->addLayout(btnLayout);
}
void MemberPropsDialog::onSave() {
if (m_edPrivkey.size() != 32 || !m_db->m_db) return;
// собираем adm_tags
QString tags;
if (m_adminCb->isChecked()) tags += "admin=yes";
if (m_superCb->isChecked()) {
if (!tags.isEmpty()) tags += ",";
tags += "supernode=yes";
}
// подписываем: tags || node_id(8 LE)
QByteArray msg = tags.toUtf8();
uint64_t le = htole64(m_nodeId);
msg.append((const char*)&le, 8);
uint8_t sig[64] = {};
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr,
(const uint8_t*)m_edPrivkey.constData(), 32);
if (pkey) {
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (mdctx) {
size_t siglen = 64;
if (EVP_DigestSignInit(mdctx, nullptr, nullptr, nullptr, pkey) == 1)
EVP_DigestSign(mdctx, sig, &siglen, (const uint8_t*)msg.constData(), msg.size());
EVP_MD_CTX_free(mdctx);
}
EVP_PKEY_free(pkey);
}
// пишем в БД
QString tbl = m_db->peersTableName(m_channelId);
QByteArray sql = QString(
"UPDATE \"%1\" SET adm_tags=?, adm_tags_sig=?, node_type=? WHERE node_id=?"
).arg(tbl).toUtf8();
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &st, nullptr) == SQLITE_OK) {
sqlite3_bind_text(st, 1, tags.isEmpty() ? nullptr : tags.toUtf8().constData(),
-1, tags.isEmpty() ? SQLITE_STATIC : SQLITE_TRANSIENT);
sqlite3_bind_blob(st, 2, sig, 64, SQLITE_TRANSIENT);
int nt = m_superCb->isChecked() ? 4 : 1;
sqlite3_bind_int(st, 3, nt);
sqlite3_bind_int64(st, 4, (sqlite3_int64)m_nodeId);
sqlite3_step(st);
sqlite3_finalize(st);
}
accept();
}

27
tools/chatgui/src/memberpropsdialog.h

@ -0,0 +1,27 @@
#pragma once
#include <QDialog>
#include <QCheckBox>
#include <QString>
class DbManager;
class MemberPropsDialog : public QDialog {
Q_OBJECT
public:
explicit MemberPropsDialog(DbManager* db, const QString& channelId,
quint64 nodeId, const QString& memberName,
QWidget* parent = nullptr);
private slots:
void onSave();
private:
DbManager* m_db;
QString m_channelId;
quint64 m_nodeId;
QByteArray m_edPrivkey;
QCheckBox* m_adminCb;
QCheckBox* m_superCb;
};
Loading…
Cancel
Save