etcp: переупорядочить close + защита state==2 во всех API-точках; topo: правильная очистка group nodes и registry; chat/member: новый модуль member_sync; chatgui: обновления транспорта и UI

This commit is contained in:
evgeny committed 2026-08-09 12:39:53 +03:00
1 parent b2e4aadbf0
commit 7f901c01e1
38 files changed
+1275 -540

No files matched your search

@@ -433,7 +433,8 @@ int instance_lite_start(const char* config_text) {
debug_config_init();
debug_set_level(DEBUG_LEVEL_INFO);
debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_INFO);
debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_INFO);
debug_set_category_level(DEBUG_CATEGORY_CHAT_SYNC, DEBUG_LEVEL_TRACE);
debug_set_category_level(DEBUG_CATEGORY_MEMBER_SYNC, DEBUG_LEVEL_TRACE);
debug_enable_function_name(0);
cfg_get_val(config_text, "db_path", g_db_path, sizeof(g_db_path));
-44
View File
@@ -258,50 +258,6 @@ MessageRow DbManager::getMessageById(const QString& chId, qint64 msgId) const {
/* ── peers (UI) ── */
int DbManager::getChannelMemberCount(const QString& chId) const {
QString sql = QStringLiteral("SELECT COUNT(*) FROM \"%1\"").arg(peersTableName(chId));
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
if (!stmt) return 0;
int c = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) c = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return c;
}
QList<ChannelMember> DbManager::getChannelMembers(const QString& chId, int offset, int limit) const {
QList<ChannelMember> list;
QString sql = QStringLiteral(
"SELECT p.node_id, COALESCE(n.online,0), COALESCE(n.name,''), COALESCE(p.local_nick,''), COALESCE(p.adm_tags,'')"
" FROM \"%1\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
" ORDER BY n.online DESC, p.node_id ASC LIMIT ? OFFSET ?"
).arg(peersTableName(chId));
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
if (!stmt) return list;
sqlite3_bind_int(stmt, 1, limit);
sqlite3_bind_int(stmt, 2, offset);
while (sqlite3_step(stmt) == SQLITE_ROW) {
ChannelMember m;
m.nodeId = (quint64)sqlite3_column_int64(stmt, 0);
m.online = sqlite3_column_int(stmt, 1) != 0;
m.name = colText(stmt, 2);
m.localNick = colText(stmt, 3);
m.admTags = colText(stmt, 4);
m.flags = 0;
const QByteArray at = m.admTags.toUtf8();
const char* tags = at.constData();
if (tags && tags[0]) {
if (strstr(tags, "\"supernode\":\"yes\"")) m.flags |= 0x01;
const char* ap = strstr(tags, "\"admin\":\"");
if (ap) { ap += 8; const char* last = ap; while (*last && *last != '"') { if (*last == 'e' || *last == 'd') ap = last; last++; } if (*ap == 'e') m.flags |= 0x02; }
const char* mp = strstr(tags, "\"moder\":\"");
if (mp) { mp += 8; const char* last = mp; while (*last && *last != '"') { if (*last == 'e' || *last == 'd') mp = last; last++; } if (*mp == 'e') m.flags |= 0x04; }
}
list.append(m);
}
sqlite3_finalize(stmt);
return list;
}
QMap<quint64, QString> DbManager::getLocalNicknames(const QString& chId) const {
QMap<quint64, QString> m;
if (!m_db) return m;
-11
View File
@@ -49,15 +49,6 @@ struct AccountRow {
bool isContact = true;
};
struct ChannelMember {
quint64 nodeId = 0;
bool online = false;
QString name; // from nodes.name, empty if not set
QString localNick; // from peers.local_nick override, empty if not set
QString admTags; // raw JSON {supernode, admin, moder, ver, ...}
uint8_t flags = 0; // bitmask: MEMBER_FLAG_SUPERNODE/ADMIN/MODER
};
struct NodeAddr {
quint64 nodeId = 0;
int family = 0;
@@ -103,8 +94,6 @@ public:
MessageRow getMessageById(const QString& chId, qint64 msgId) const;
/* ── Участники канала (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;
QMap<quint64, QString> getLocalNicknames(const QString& chId) const;
+142 -115
View File
@@ -4,6 +4,7 @@
#include "memberpropsdialog.h"
#include "renamedialog.h"
#include "../db/db_manager.h"
#include "../transport/gui_bridge.h"
#include <QVBoxLayout>
#include <QSplitter>
#include <QLabel>
@@ -11,15 +12,23 @@
#include <QFont>
#include <QSet>
#include <algorithm>
#include <cstring>
#include "../../../lib/platform_compat.h"
extern "C" {
#include "utun_instance.h"
#include "etcp.h"
#include "etcp_connections.h"
#include "../lib/socket_compat.h"
#include "../lib/ll_queue.h"
#include "stcp_link.h"
#include "../lib/debug_config.h"
#include "chat/chat_core.h"
#include "../../lib/mem.h"
}
static AccountList* g_detailTarget = nullptr;
static void memberDetailCb(const uint8_t* data, int len) {
if (!g_detailTarget) return;
if (len < 13) return;
uint64_t nodeId; memcpy(&nodeId, data, 8);
g_detailTarget->onMemberDetailData(nodeId, data, len);
}
QString AccountList::addrStr(const void* addr, int family) {
@@ -51,7 +60,7 @@ QString AccountList::relativeTimeStr(qint64 timestamp) {
AccountList::AccountList(DbManager* db, QWidget *parent)
: QWidget(parent)
, m_listView(new QListView(this))
, m_model(new MemberListModel(db, this))
, m_model(new MemberListModel(this))
, m_db(db)
, m_detailText(nullptr)
{
@@ -93,10 +102,6 @@ AccountList::AccountList(DbManager* db, QWidget *parent)
connect(m_listView, &QListView::clicked, this, &AccountList::onMemberClicked);
connect(m_listView, &QListView::doubleClicked, this, &AccountList::onMemberDoubleClicked);
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);
@@ -107,8 +112,15 @@ AccountList::AccountList(DbManager* db, QWidget *parent)
m_linkTimer = new QTimer(this);
m_linkTimer->setInterval(1000);
connect(m_linkTimer, &QTimer::timeout, this, [this]{
if (m_detailNodeId != 0) updateDetailDisplay();
if (m_detailNodeId != 0) requestMemberDetail();
});
g_detailTarget = this;
gui_bridge_set_member_detail_cb(memberDetailCb);
}
AccountList::~AccountList() {
if (g_detailTarget == this) g_detailTarget = nullptr;
}
void AccountList::setChannel(const QString& channelId) {
@@ -139,6 +151,12 @@ void AccountList::showMemberDetail(quint64 nodeId) {
m_detailLastSeen = 0;
m_detailCreated = 0;
m_detailAddrs.clear();
m_snapConnPresent = false;
m_snapConnUp = false;
m_snapConnInit = false;
m_snapOwnTcpActive = 0;
m_snapLinks.clear();
m_snapSocks.clear();
sqlite3* d = m_db->m_db;
sqlite3_stmt* st = nullptr;
@@ -193,31 +211,6 @@ void AccountList::showMemberDetail(quint64 nodeId) {
sqlite3_finalize(na);
}
/* resolve interface names from live sockets and sort */
if (m_inst) {
for (auto& a : m_detailAddrs) {
if (a.protocol == 1) {
for (struct ETCP_SOCKET* s = m_inst->etcp_sockets; s; s = s->next) {
if ((int)s->sock_id == a.socket_id) {
QString name = QString::fromLatin1(s->name);
int idx = name.lastIndexOf("_v");
if (idx > 3) a.ifname = name.mid(3, idx - 3);
break;
}
}
} else if (a.protocol == 2) {
for (struct TCP_SOCKET* ts = m_inst->tcp_sockets; ts; ts = ts->next) {
if ((int)ts->sock_id == a.socket_id) {
QString name = QString::fromLatin1(ts->name);
int idx = name.lastIndexOf("_v");
if (idx > 3) a.ifname = name.mid(3, idx - 3);
break;
}
}
}
}
}
std::sort(m_detailAddrs.begin(), m_detailAddrs.end(),
[](const DetailAddress& a, const DetailAddress& b) {
if (a.ifname != b.ifname) return a.ifname < b.ifname;
@@ -229,6 +222,78 @@ void AccountList::showMemberDetail(quint64 nodeId) {
m_detailTimer->start();
m_linkTimer->start();
updateDetailDisplay();
requestMemberDetail();
}
void AccountList::requestMemberDetail() {
if (m_detailNodeId == 0) return;
void* arg = u_malloc(sizeof(uint64_t));
if (!arg) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "AccountList: req alloc failed"); return; }
uint64_t nid = m_detailNodeId;
memcpy(arg, &nid, sizeof(nid));
gui_bridge_post_uasync_fn(chat_core_collect_member_detail_trampoline, arg);
}
void AccountList::onMemberDetailData(uint64_t nodeId, const uint8_t* data, int len) {
if (nodeId != m_detailNodeId) return; /* stale response from earlier selection */
if (len < 13) return;
const uint8_t* p = data;
/* skip node_id(8) */
p += 8;
uint8_t flags = *p++;
m_snapConnPresent = (flags & 1) != 0;
m_snapConnUp = (flags & 2) != 0;
m_snapConnInit = (flags & 4) != 0;
uint16_t ownTcpActive;
memcpy(&ownTcpActive, p, 2); p += 2;
m_snapOwnTcpActive = ownTcpActive;
uint8_t linkCount = *p++;
m_snapLinks.clear();
for (int i = 0; i < linkCount; i++) {
if (p + MEMBER_DETAIL_LINK_SIZE > data + len) break;
MemberLinkSnap lk;
lk.isTcp = p[0] != 0;
lk.linkState = p[1];
lk.linkStatus = p[2];
lk.tcpReady = p[3] != 0;
lk.family = p[4];
int addrLen = p[5];
lk.addr = QByteArray((const char*)p + 6, qMin(addrLen, 16));
memcpy(&lk.port, p + 22, 2);
p += MEMBER_DETAIL_LINK_SIZE;
m_snapLinks.append(lk);
}
if (p >= data + len) { updateDetailDisplay(); return; }
uint8_t sockCount = *p++;
m_snapSocks.clear();
for (int i = 0; i < sockCount; i++) {
if (p + MEMBER_DETAIL_SOCK_SIZE > data + len) break;
MemberSockSnap s;
memcpy(&s.sockId, p, 4);
memcpy(&s.port, p + 4, 2);
memcpy(&s.linkCount, p + 6, 4);
s.ifname = QString::fromUtf8((const char*)p + 10).trimmed();
p += MEMBER_DETAIL_SOCK_SIZE;
m_snapSocks.append(s);
}
/* resolve interface names from snapshot sockets (by socket_id) */
for (auto& a : m_detailAddrs) {
for (const auto& s : m_snapSocks) {
if (s.sockId == a.socket_id) { a.ifname = s.ifname; break; }
}
}
std::sort(m_detailAddrs.begin(), m_detailAddrs.end(),
[](const DetailAddress& a, const DetailAddress& b) {
if (a.ifname != b.ifname) return a.ifname < b.ifname;
if (a.addr != b.addr) return a.addr < b.addr;
if (a.protocol != b.protocol) return a.protocol < b.protocol;
return a.port < b.port;
});
updateDetailDisplay();
}
void AccountList::updateDetailDisplay() {
@@ -246,83 +311,36 @@ void AccountList::updateDetailDisplay() {
if (m_detailCreated > 0)
lines.append(QString("📅 %1").arg(relativeTimeStr(m_detailCreated)));
struct ETCP_CONN* conn = nullptr;
if (m_inst) conn = instance_find_conn(m_inst, m_detailNodeId);
bool isSelf = m_db ? (m_detailNodeId == m_db->myNodeId()) : false;
QSet<struct ETCP_LINK*> matchedLinks;
/* Count active TCP connections for own node (shared across all TCP sockets) */
int ownTcpActiveLinks = 0;
if (isSelf && m_inst) {
if (m_inst->connections) {
for (struct ll_entry* e = m_inst->connections->head; e; e = e->next) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data;
if (ce && ce->conn && ce->conn->initialized) {
for (struct ETCP_LINK* lk = ce->conn->links; lk; lk = lk->next) {
if (lk->is_tcp) { ownTcpActiveLinks++; break; }
}
}
}
}
if (m_inst->tcp_connections) {
for (struct ll_entry* e = m_inst->tcp_connections->head; e; e = e->next) {
struct tcp_conn_entry* te = (struct tcp_conn_entry*)e->data;
if (te && te->etcp_conn && te->etcp_conn->initialized) ownTcpActiveLinks++;
}
}
}
for (const auto& a : m_detailAddrs) {
QString proto;
switch (a.protocol) { case 1: proto = "UDP"; break; case 2: proto = "TCP"; break; default: proto = "UDP/TCP"; break; }
QString linkColor = "#9E9E9E";
if (conn) {
for (struct ETCP_LINK* lk = conn->links; lk; lk = lk->next) {
const struct sockaddr_storage* sa = &lk->remote_addr;
int lfm = 0; QString lip; uint16_t lpo = 0;
char ipbuf[INET6_ADDRSTRLEN];
if (sa->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
inet_ntop(AF_INET, &sin->sin_addr, ipbuf, sizeof(ipbuf));
lfm = 4; lpo = ntohs(sin->sin_port); lip = QString::fromLocal8Bit(ipbuf);
} else if (sa->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
inet_ntop(AF_INET6, &sin6->sin6_addr, ipbuf, sizeof(ipbuf));
lfm = 6; lpo = ntohs(sin6->sin6_port); lip = QString::fromLocal8Bit(ipbuf);
}
if (lfm == a.family && lpo == (uint16_t)a.port && lip == addrStr(a.addr.constData(), a.family)) {
linkColor = (lk->link_state == 3 && lk->link_status == 1) ? "#4CAF50" : "#E53935";
matchedLinks.insert(lk);
if (m_snapConnPresent) {
for (const auto& lk : m_snapLinks) {
if (lk.family == a.family && lk.port == a.port
&& lk.addr == a.addr) {
linkColor = (lk.linkState == 3 && lk.linkStatus == 1) ? "#4CAF50" : "#E53935";
break;
}
}
if (linkColor == "#9E9E9E" && a.protocol == 2) {
for (struct ETCP_LINK* tlk = conn->links; tlk; tlk = tlk->next) {
if (tlk->is_tcp && tlk->tcp_link) {
linkColor = stcp_link_is_ready(tlk->tcp_link) ? "#4CAF50" : "#E53935";
break;
}
for (const auto& lk : m_snapLinks) {
if (lk.isTcp && lk.tcpReady) { linkColor = "#4CAF50"; break; }
}
}
}
QString prefix;
if (isSelf && m_inst) {
if (isSelf) {
int linkCount = 0;
if (a.protocol == 1) {
for (struct ETCP_SOCKET* sock = m_inst->etcp_sockets; sock; sock = sock->next) {
uint16_t sp = 0;
const struct sockaddr_storage* addr = sock->interface_addr.ss_family ? &sock->interface_addr : &sock->local_addr;
if (addr->ss_family == AF_INET) sp = ntohs(((const struct sockaddr_in*)addr)->sin_port);
else if (addr->ss_family == AF_INET6) sp = ntohs(((const struct sockaddr_in6*)addr)->sin6_port);
if (sp == (uint16_t)a.port) {
linkCount = sock->links_queue ? queue_entry_count(sock->links_queue) : 0;
break;
}
for (const auto& s : m_snapSocks) {
if (s.port == a.port) { linkCount = s.linkCount; break; }
}
} else if (a.protocol == 2) {
linkCount = ownTcpActiveLinks;
linkCount = m_snapOwnTcpActive;
}
if (linkCount > 0) prefix = QString("[%1] ").arg(linkCount);
}
@@ -338,25 +356,20 @@ void AccountList::updateDetailDisplay() {
}
}
/* Show ETCP links that are not in DB (non-self) */
if (conn) {
for (struct ETCP_LINK* lk = conn->links; lk; lk = lk->next) {
if (matchedLinks.contains(lk)) continue;
const struct sockaddr_storage* sa = &lk->remote_addr;
char ipbuf[INET6_ADDRSTRLEN]; QString lip; uint16_t lpo = 0;
if (sa->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
inet_ntop(AF_INET, &sin->sin_addr, ipbuf, sizeof(ipbuf));
lpo = ntohs(sin->sin_port); lip = QString::fromLocal8Bit(ipbuf);
} else if (sa->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
inet_ntop(AF_INET6, &sin6->sin6_addr, ipbuf, sizeof(ipbuf));
lpo = ntohs(sin6->sin6_port); lip = QString::fromLocal8Bit(ipbuf);
} else { continue; }
QString proto = lk->is_tcp ? "TCP" : "UDP";
QString lc = (lk->link_state == 3 && lk->link_status == 1) ? "#4CAF50" : "#E53935";
/* Show snapshot links that are not in DB (non-self) */
if (m_snapConnPresent && m_snapLinks.size() > 0) {
for (int li = 0; li < m_snapLinks.size(); li++) {
const auto& lk = m_snapLinks[li];
bool matched = false;
for (const auto& a : m_detailAddrs) {
if (lk.family == a.family && lk.port == a.port
&& lk.addr == a.addr) { matched = true; break; }
}
if (matched) continue;
QString proto = lk.isTcp ? "TCP" : "UDP";
QString lc = (lk.linkState == 3 && lk.linkStatus == 1) ? "#4CAF50" : "#E53935";
lines.append(QString("<span style='color:%1;'>●</span> %2:%3:%4 [link]")
.arg(lc, proto, lip).arg(lpo));
.arg(lc, proto, addrStr(lk.addr.constData(), lk.family)).arg(lk.port));
}
}
@@ -364,7 +377,7 @@ void AccountList::updateDetailDisplay() {
}
void AccountList::onDetailTimerTick() {
if (m_detailNodeId != 0) updateDetailDisplay();
if (m_detailNodeId != 0) requestMemberDetail();
}
void AccountList::onMemberDoubleClicked(const QModelIndex& index) {
@@ -372,7 +385,8 @@ void AccountList::onMemberDoubleClicked(const QModelIndex& index) {
quint64 nodeId = m_model->nodeIdAt(index.row());
if (!nodeId) return;
QString name = m_model->data(index, Qt::DisplayRole).toString();
MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, this);
uint8_t flags = (uint8_t)(m_model->data(index, Qt::UserRole + 3).toInt() & 0xFF);
MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, flags, this);
if (dlg.exec() == QDialog::Accepted) {
m_model->refresh();
emit memberRenamed();
@@ -404,9 +418,22 @@ void AccountList::onMemberContextMenu(const QPoint& pos) {
}
if (chosen != editAction) return;
MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, this);
uint8_t flags = (uint8_t)(m_model->data(index, Qt::UserRole + 3).toInt() & 0xFF);
MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, flags, this);
if (dlg.exec() == QDialog::Accepted) {
m_model->refresh();
emit memberRenamed();
}
}
void AccountList::onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen) {
m_model->onMemberList(chId, count, data, dataLen);
}
void AccountList::onMemberUpdated(const QString& chId, const uint8_t* data) {
m_model->onMemberUpdated(chId, data);
}
void AccountList::onMemberRemoved(const QString& chId, uint64_t nodeId) {
m_model->onMemberRemoved(chId, nodeId);
}
+35 -5
View File
@@ -7,10 +7,10 @@
#include <QMenu>
#include <QVector>
#include <QElapsedTimer>
#include <stdint.h>
class MemberListModel;
class DbManager;
struct UTUN_INSTANCE;
struct DetailAddress {
int family;
@@ -21,14 +21,39 @@ struct DetailAddress {
QString ifname;
};
struct MemberLinkSnap {
bool isTcp;
int linkState;
int linkStatus;
bool tcpReady;
int family;
QByteArray addr;
int port;
};
struct MemberSockSnap {
int sockId;
int port;
int linkCount;
QString ifname;
};
class AccountList : public QWidget {
Q_OBJECT
public:
explicit AccountList(DbManager* db = nullptr, QWidget *parent = nullptr);
~AccountList();
void setChannel(const QString& channelId);
void refresh();
void setUtunInstance(struct UTUN_INSTANCE* inst) { m_inst = inst; }
void onMemberDetailData(uint64_t nodeId, const uint8_t* data, int len);
/* Bridge callbacks (called from GUI thread) */
void onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen);
void onMemberUpdated(const QString& chId, const uint8_t* data);
void onMemberRemoved(const QString& chId, uint64_t nodeId);
MemberListModel* model() const { return m_model; }
signals:
void memberRenamed();
@@ -42,6 +67,7 @@ private slots:
private:
void showMemberDetail(quint64 nodeId);
void updateDetailDisplay();
void requestMemberDetail();
static QString relativeTimeStr(qint64 timestamp);
static QString addrStr(const void* addr, int family);
@@ -49,17 +75,21 @@ private:
MemberListModel *m_model;
DbManager *m_db;
QTextEdit *m_detailText;
QTimer *m_refreshTimer;
QTimer *m_detailTimer;
QTimer *m_linkTimer;
QString m_channelId;
struct UTUN_INSTANCE* m_inst = nullptr;
quint64 m_detailNodeId = 0;
QString m_detailName;
bool m_detailOnline = false;
qint64 m_detailLastSeen = 0;
qint64 m_detailCreated = 0;
QVector<DetailAddress> m_detailAddrs;
bool m_snapConnPresent = false;
bool m_snapConnUp = false;
bool m_snapConnInit = false;
int m_snapOwnTcpActive = 0;
QVector<MemberLinkSnap> m_snapLinks;
QVector<MemberSockSnap> m_snapSocks;
};
+18 -29
View File
@@ -8,21 +8,21 @@ ChannelDelegate::ChannelDelegate(QObject *parent)
QRect ChannelDelegate::inviteButtonRect(const QRect &itemRect) {
const int margin = 6;
return QRect(itemRect.right() - margin - InviteBtnSize,
itemRect.bottom() - margin - InviteBtnSize,
itemRect.top() + margin,
InviteBtnSize, InviteBtnSize);
}
QRect ChannelDelegate::inviteByLinkButtonRect(const QRect &itemRect) {
const int margin = 6;
return QRect(itemRect.right() - margin - InviteBtnSize * 2 - InviteBtnGap,
itemRect.bottom() - margin - InviteBtnSize,
itemRect.top() + margin,
InviteBtnSize, InviteBtnSize);
}
QRect ChannelDelegate::settingsButtonRect(const QRect &itemRect) {
const int margin = 6;
return QRect(itemRect.right() - margin - InviteBtnSize * 3 - InviteBtnGap * 2,
itemRect.bottom() - margin - InviteBtnSize,
itemRect.top() + margin,
InviteBtnSize, InviteBtnSize);
}
@@ -99,6 +99,7 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
QRect nameRect = textRect;
nameRect.setHeight(QFontMetrics(nameFont).height());
nameRect.setRight(nameRect.right() - InviteBtnSize * 3 - InviteBtnGap * 2 - 4);
painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignTop,
painter->fontMetrics().elidedText(name, Qt::ElideRight, nameRect.width()));
@@ -113,7 +114,7 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
if (option.state & QStyle::State_Selected) nickColor = nickColor.lighter(140);
QRect msgRect(textRect.left(), nameRect.bottom() + 4,
textRect.right() - InviteBtnSize - 6 - textRect.left(),
textRect.width(),
option.rect.bottom() - nameRect.bottom() - margin);
QFontMetrics msgFm(msgFont);
@@ -163,21 +164,15 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
painter->drawText(badgeRect, Qt::AlignCenter, unreadText);
}
/* ── invite plus button (bottom-right) ── */
/* ── invite plus button (top-right) ── */
QRect plusRect = inviteButtonRect(option.rect);
bool plusHovered = index.data(ChannelInviteBtnHoveredRole).toInt() != 0;
painter->setRenderHint(QPainter::Antialiasing);
if (plusHovered) {
painter->setBrush(QColor(0x33, 0x90, 0xEC));
painter->setPen(Qt::NoPen);
painter->drawEllipse(plusRect);
painter->setPen(QPen(Qt::white, 2));
} else {
painter->setBrush(QColor(0x33, 0x90, 0xEC, 110));
painter->setPen(Qt::NoPen);
painter->drawEllipse(plusRect);
painter->setPen(QPen(QColor(0x33, 0x90, 0xEC), 2));
}
QColor plusFill = plusHovered ? QColor(0x4A, 0xA4, 0xF0) : QColor(0x33, 0x90, 0xEC);
painter->setBrush(plusFill);
painter->setPen(Qt::NoPen);
painter->drawEllipse(plusRect);
painter->setPen(QPen(Qt::white, 2));
painter->drawLine(plusRect.center().x(), plusRect.top() + 5,
plusRect.center().x(), plusRect.bottom() - 5);
painter->drawLine(plusRect.left() + 5, plusRect.center().y(),
@@ -187,18 +182,12 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
QRect invRect = inviteByLinkButtonRect(option.rect);
bool invHovered = index.data(ChannelInviteByLinkBtnHoveredRole).toInt() != 0;
painter->setRenderHint(QPainter::Antialiasing);
if (invHovered) {
painter->setBrush(QColor(0xE6, 0x7E, 0x22));
painter->setPen(Qt::NoPen);
painter->drawRoundedRect(invRect, 3, 3);
painter->setPen(QPen(Qt::white, 1.5));
} else {
painter->setBrush(QColor(0xE6, 0x7E, 0x22, 110));
painter->setPen(Qt::NoPen);
painter->drawRoundedRect(invRect, 3, 3);
painter->setPen(QPen(QColor(0xE6, 0x7E, 0x22), 1.5));
}
/* arrow pointing into a box: →| or a simple arrow-left: ← */
QColor invFill = invHovered ? QColor(0xF0, 0x90, 0x30) : QColor(0xE6, 0x7E, 0x22);
painter->setBrush(invFill);
painter->setPen(Qt::NoPen);
painter->drawRoundedRect(invRect, 3, 3);
painter->setPen(QPen(Qt::white, 1.5));
/* arrow: ← */
int cx = invRect.center().x(), cy = invRect.center().y();
int dx = 3;
painter->drawLine(cx + dx, cy - 3, cx - dx, cy);
@@ -209,7 +198,7 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
/* ── settings gear (left of invite-by-link button) ── */
QRect gearRect = settingsButtonRect(option.rect);
bool gearHovered = index.data(ChannelSettingsBtnHoveredRole).toInt() != 0;
QColor gearColor = gearHovered ? QColor("#3390EC") : QColor("#888");
QColor gearColor = gearHovered ? QColor("#3390EC") : QColor("#555");
painter->setPen(QPen(gearColor, 2));
painter->setBrush(Qt::NoBrush);
int gr = 5;
+10 -10
View File
@@ -149,8 +149,8 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent)
gearBtn->setFixedSize(28, 28);
gearBtn->setToolTip(QString::fromUtf8("Настройки"));
gearBtn->setStyleSheet(
"QPushButton { border: none; font-size: 18px; background: transparent; color: gray; }"
"QPushButton:hover { color: palette(text); }");
"QPushButton { border: none; font-size: 18px; background: transparent; color: #666; }"
"QPushButton:hover { color: #3390EC; background: rgba(51,144,236,20); border-radius: 14px; }");
fl->addWidget(gearBtn);
connect(gearBtn, &QPushButton::clicked, this, [this] { emit settingsRequested(); });
@@ -159,8 +159,8 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent)
monitorBtn->setFixedSize(28, 28);
monitorBtn->setToolTip(QString::fromUtf8("Connection Monitor"));
monitorBtn->setStyleSheet(
"QPushButton { border: none; font-size: 16px; background: transparent; color: gray; }"
"QPushButton:hover { color: palette(text); }");
"QPushButton { border: none; font-size: 16px; background: transparent; color: #666; }"
"QPushButton:hover { color: #3390EC; background: rgba(51,144,236,20); border-radius: 14px; }");
fl->addWidget(monitorBtn);
connect(monitorBtn, &QPushButton::clicked, this, [this] { emit connMonitorRequested(); });
@@ -170,9 +170,9 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent)
createBtn->setFixedSize(36, 28);
createBtn->setToolTip(QString::fromUtf8("Создать группу"));
createBtn->setStyleSheet(
"QPushButton { border: 1.5px solid #27AE60; border-radius: 6px;"
" font-size: 11px; font-weight: bold; color: #27AE60; background: transparent; }"
"QPushButton:hover { background: #27AE60; color: white; }");
"QPushButton { border: none; border-radius: 6px; font-size: 11px; font-weight: bold;"
" color: white; background: #27AE60; }"
"QPushButton:hover { background: #219A52; }");
connect(createBtn, &QPushButton::clicked, this, [this] { emit createGroupRequested(); });
fl->addWidget(createBtn);
@@ -180,9 +180,9 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent)
plusBtn->setFixedSize(28, 28);
plusBtn->setToolTip(QString::fromUtf8("Присоединиться"));
plusBtn->setStyleSheet(
"QPushButton { border: 1.5px solid #3390EC; border-radius: 14px;"
" font-size: 18px; font-weight: bold; color: #3390EC; background: transparent; }"
"QPushButton:hover { background: #3390EC; color: white; }");
"QPushButton { border: none; border-radius: 14px; font-size: 18px; font-weight: bold;"
" color: white; background: #3390EC; }"
"QPushButton:hover { background: #2B7EC8; }");
connect(plusBtn, &QPushButton::clicked, this, [this] { emit joinRequested(); });
fl->addWidget(plusBtn);
+39 -11
View File
@@ -60,10 +60,30 @@ static void onChannelUpdatedCallback(const char* ch_id, int ch_id_len) {
s_mainWindow->reloadChannels();
}
static void onMembersChangedCallback(const char* ch_id, int ch_id_len) {
Q_UNUSED(ch_id_len);
static void onMemberListCallback(const uint8_t* data, int len) {
if (len < 3) return;
uint8_t cl = data[0];
if (len < 1 + (int)cl + 2) return;
uint16_t count; memcpy(&count, data + 1 + cl, 2);
if (s_mainWindow)
s_mainWindow->onMembersChanged(ch_id);
s_mainWindow->onMemberList(QString::fromUtf8((const char*)data + 1, cl), count, data + 1 + cl + 2, len - 1 - cl - 2);
}
static void onMemberUpdatedCallback(const uint8_t* data, int len) {
if (len < 1 + 64 + 1 + 79) return;
uint8_t cl = data[0];
if (len < 1 + (int)cl + 1 + 79) return;
if (s_mainWindow)
s_mainWindow->onMemberUpdated(QString::fromUtf8((const char*)data + 1, cl), data + 1 + cl + 1);
}
static void onMemberRemovedCallback(const uint8_t* data, int len) {
if (len < 1 + 64 + 8) return;
uint8_t cl = data[0];
if (len < 1 + (int)cl + 8) return;
uint64_t nid; memcpy(&nid, data + 1 + cl, 8);
if (s_mainWindow)
s_mainWindow->onMemberRemoved(QString::fromUtf8((const char*)data + 1, cl), nid);
}
static void onAutoConnectStatusCallback(uint8_t status, uint16_t total_tried,
@@ -109,7 +129,9 @@ MainWindow::~MainWindow() {
m_autoPlayTimer->stop();
gui_bridge_set_msg_received_cb(nullptr);
gui_bridge_set_channel_updated_cb(nullptr);
gui_bridge_set_members_changed_cb(nullptr);
gui_bridge_set_member_list_cb(nullptr);
gui_bridge_set_member_updated_cb(nullptr);
gui_bridge_set_member_removed_cb(nullptr);
gui_bridge_set_my_node_id_cb(nullptr);
gui_bridge_set_auto_connect_status_cb(nullptr);
gui_bridge_set_channel_peers_online_cb(nullptr);
@@ -157,11 +179,9 @@ void MainWindow::setupNode() {
});
connect(m_node, &UtunNode::started, this, [this]() {
qDebug("UtunNode started");
if (m_accountList && m_node) m_accountList->setUtunInstance(m_node->instance());
});
connect(m_node, &UtunNode::stopped, this, [this]() {
qDebug("UtunNode stopped");
if (m_node) m_node->finalize();
qApp->quit();
});
@@ -260,7 +280,9 @@ void MainWindow::setupBridgeCallbacks() {
s_mainWindow = this;
gui_bridge_set_msg_received_cb(onMsgReceivedCallback);
gui_bridge_set_channel_updated_cb(onChannelUpdatedCallback);
gui_bridge_set_members_changed_cb(onMembersChangedCallback);
gui_bridge_set_member_list_cb(onMemberListCallback);
gui_bridge_set_member_updated_cb(onMemberUpdatedCallback);
gui_bridge_set_member_removed_cb(onMemberRemovedCallback);
gui_bridge_set_my_node_id_cb([](uint64_t nid) {
qDebug("MainWindow: MY_NODE_ID setMyNodeId(%016llx)", (unsigned long long)nid);
if (s_mainWindow && s_mainWindow->m_db) s_mainWindow->m_db->setMyNodeId((quint64)nid);
@@ -463,10 +485,16 @@ void MainWindow::onMessageReceived(const char* ch_id, int ch_id_len, uint64_t au
}
}
void MainWindow::onMembersChanged(const char* ch_id) {
QString cur = QSettings().value("last_channel_id").toString();
if (cur == QString::fromUtf8(ch_id))
m_accountList->refresh();
void MainWindow::onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen) {
m_accountList->onMemberList(chId, count, data, dataLen);
}
void MainWindow::onMemberUpdated(const QString& chId, const uint8_t* data) {
m_accountList->onMemberUpdated(chId, data);
}
void MainWindow::onMemberRemoved(const QString& chId, uint64_t nodeId) {
m_accountList->onMemberRemoved(chId, nodeId);
}
void MainWindow::onAutoConnectStatus(uint8_t status, uint16_t /*total_tried*/,
+3 -1
View File
@@ -48,7 +48,9 @@ private slots:
public:
void onMessageReceived(const char* ch_id, int ch_id_len, uint64_t author);
void onMembersChanged(const char* ch_id);
void onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen);
void onMemberUpdated(const QString& chId, const uint8_t* data);
void onMemberRemoved(const QString& chId, uint64_t nodeId);
void onAutoConnectStatus(uint8_t status, uint16_t total, uint16_t totalNodes, uint16_t connected);
void onChannelPeersOnline(const char* ch_id, int ch_id_len, uint16_t online);
void reloadChannels();
+108 -143
View File
@@ -1,11 +1,26 @@
#include "memberlistmodel.h"
#include "../db/db_manager.h"
#include "../transport/gui_bridge.h"
#include <QPainter>
#include <QFont>
#include <QTimer>
#include <QColor>
#include <cstring>
MemberListModel::MemberListModel(DbManager* db, QObject* parent)
: QAbstractListModel(parent), m_db(db) {}
extern "C" {
#include "../../../src/chat/chat_core.h"
#include "../../../src/chat/chat_member.h"
#include "../../../lib/mem.h"
}
#define CHAT_MEMBER_DISPLAY_SIZE 79
MemberListModel::MemberListModel(QObject* parent)
: QAbstractListModel(parent)
, m_rttTimer(new QTimer(this))
{
m_rttTimer->setInterval(2000);
connect(m_rttTimer, &QTimer::timeout, this, &MemberListModel::refreshRtt);
m_rttTimer->start();
}
int MemberListModel::rowCount(const QModelIndex& parent) const {
(void)parent;
@@ -18,7 +33,7 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const {
auto it = m_cache.find(index.row());
if (it != m_cache.end()) {
if (role == Qt::DisplayRole) return it->localNick.isEmpty() ? it->name : it->localNick;
if (role == Qt::DisplayRole) return it->name;
if (role == Qt::DecorationRole) return QVariant::fromValue(QIcon(it->avatar));
if (role == Qt::UserRole + 1) {
QString sub;
@@ -38,168 +53,118 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const {
if (role == Qt::UserRole + 3) return it->flags;
return {};
}
if (role == Qt::DisplayRole) {
scheduleBatch(index.row());
return QStringLiteral("\u2026");
}
return {};
}
void MemberListModel::setChannel(const QString& channelId) {
beginResetModel();
m_channelId = channelId;
m_db->cleanOrphanedPeers(channelId);
m_totalCount = m_db ? m_db->getChannelMemberCount(channelId) : 0;
m_totalCount = 0;
m_cache.clear();
m_pendingFrom = -1;
endResetModel();
if (m_totalCount > 0) scheduleBatch(0);
if (!channelId.isEmpty()) {
char* ch = u_strdup(channelId.toUtf8().constData());
gui_bridge_post_uasync_fn(chat_core_request_member_list_trampoline, ch);
}
}
void MemberListModel::refresh() { if (!m_channelId.isEmpty()) setChannel(m_channelId); }
void MemberListModel::refresh() {
if (!m_channelId.isEmpty()) setChannel(m_channelId);
}
void MemberListModel::refreshRtt() {
if (m_channelId.isEmpty()) return;
char* ch = u_strdup(m_channelId.toUtf8().constData());
gui_bridge_post_uasync_fn(chat_core_request_member_rtt_trampoline, ch);
}
quint64 MemberListModel::nodeIdAt(int row) const {
auto it = m_cache.find(row);
return it != m_cache.end() ? it->nodeId : 0;
}
void MemberListModel::scheduleBatch(int centerRow) const {
if (m_pendingFrom >= 0) return;
int from = centerRow - BATCH / 2;
if (from < 0) from = 0;
int maxFrom = m_totalCount - BATCH;
if (from > maxFrom) from = maxFrom > 0 ? maxFrom : 0;
const_cast<MemberListModel*>(this)->m_pendingFrom = from;
const_cast<MemberListModel*>(this)->m_pendingCount = BATCH;
QTimer::singleShot(0, const_cast<MemberListModel*>(this), &MemberListModel::loadPendingBatch);
}
static void deserialize_member(const uint8_t* src, MemberListItem& item) {
memcpy(&item.nodeId, src, 8);
uint8_t online = src[8];
item.isSelf = src[9] != 0;
item.flags = src[10];
uint16_t rtt; memcpy(&rtt, src + 11, 2);
item.rtt = (rtt == 0xFFFF) ? -1 : (int)rtt;
int nat = (int)src[13];
item.natType = (nat == 0xFF) ? -1 : nat;
item.online = (online != 0);
void MemberListModel::loadPendingBatch() {
if (m_pendingFrom < 0) return;
int from = m_pendingFrom, count = m_pendingCount;
m_pendingFrom = -1;
loadBatch(from, count);
}
char name[64];
memcpy(name, src + 15, 64); name[63] = '\0';
item.name = QString::fromUtf8(name);
void MemberListModel::loadBatch(int from, int count) {
if (!m_db || m_channelId.isEmpty()) return;
int maxFrom = m_totalCount - count;
if (from > maxFrom) from = maxFrom > 0 ? maxFrom : 0;
auto members = m_db->getChannelMembers(m_channelId, from, count);
for (int i = 0; i < members.size(); i++) {
int row = from + i;
MemberListItem& item = m_cache[row];
item.nodeId = members[i].nodeId;
item.isSelf = (item.nodeId == m_db->myNodeId());
item.online = members[i].online;
item.flags = members[i].flags;
QString shortId = QString("%1").arg(members[i].nodeId, 8, 16, QChar('0')).right(4).toUpper();
item.name = members[i].name.isEmpty() ? shortId : members[i].name;
item.localNick = members[i].localNick;
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;
QList<int> toRemove;
for (auto it = m_cache.begin(); it != m_cache.end(); ++it)
if (qAbs(it.key() - centerRow) > half)
toRemove.append(it.key());
for (int k : toRemove) m_cache.remove(k);
}
QPixmap MemberListModel::makeAvatar(QColor color, QChar letter) const {
QPixmap pm(24, 24);
pm.fill(Qt::transparent);
QColor c = item.online ? QColor("#4CAF50") : QColor("#9E9E9E");
QPixmap pm(24, 24); pm.fill(Qt::transparent);
QPainter p(&pm);
p.setRenderHint(QPainter::Antialiasing);
p.setBrush(color);
p.setBrush(c);
p.setPen(Qt::NoPen);
p.drawEllipse(2, 2, 20, 20);
p.setPen(Qt::white);
QFont f; f.setBold(true); f.setPixelSize(12);
p.setFont(f);
p.drawText(QRect(0, 0, 24, 24), Qt::AlignCenter, QString(letter));
p.drawText(QRect(0, 0, 24, 24), Qt::AlignCenter, item.name.left(1));
p.end();
return pm;
item.avatar = pm;
}
void MemberListModel::onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen) {
(void)dataLen;
if (chId != m_channelId) return;
beginResetModel();
m_totalCount = count;
m_cache.clear();
for (int i = 0; i < count; i++) {
MemberListItem item;
deserialize_member(data + i * CHAT_MEMBER_DISPLAY_SIZE, item);
m_cache[i] = item;
}
endResetModel();
}
void MemberListModel::onMemberUpdated(const QString& chId, const uint8_t* data) {
if (chId != m_channelId) return;
MemberListItem item;
deserialize_member(data, item);
/* find existing row by nodeId */
int row = -1;
for (auto it = m_cache.begin(); it != m_cache.end(); ++it) {
if (it->nodeId == item.nodeId) { row = it.key(); break; }
}
if (row >= 0) {
m_cache[row] = item;
emit dataChanged(index(row), index(row));
} else {
/* new member — add at end */
row = m_totalCount++;
m_cache[row] = item;
beginInsertRows(QModelIndex(), row, row);
endInsertRows();
}
}
void MemberListModel::onMemberRemoved(const QString& chId, uint64_t nodeId) {
if (chId != m_channelId) return;
for (auto it = m_cache.begin(); it != m_cache.end(); ++it) {
if (it->nodeId == nodeId) {
int row = it.key();
beginRemoveRows(QModelIndex(), row, row);
m_cache.remove(row);
m_totalCount--;
endRemoveRows();
return;
}
}
}
+10 -16
View File
@@ -7,8 +7,8 @@
#include <QString>
#include <QIcon>
#include <QPixmap>
class DbManager;
#include <QTimer>
#include <stdint.h>
struct MemberListItem {
quint64 nodeId = 0;
@@ -16,7 +16,6 @@ struct MemberListItem {
bool isSelf = false;
QPixmap avatar;
QString name;
QString localNick;
int rtt = -1;
int natType = -1;
uint8_t flags = 0;
@@ -25,10 +24,7 @@ struct MemberListItem {
class MemberListModel : public QAbstractListModel {
Q_OBJECT
public:
static const int BATCH = 40;
static const int MAX_CACHE = 120;
explicit MemberListModel(DbManager* db, QObject* parent = nullptr);
explicit MemberListModel(QObject* parent = nullptr);
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
@@ -38,21 +34,19 @@ public:
void refreshRtt();
quint64 nodeIdAt(int row) const;
private slots:
void loadPendingBatch();
/* Called from bridge callbacks (GUI thread) */
void onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen);
void onMemberUpdated(const QString& chId, const uint8_t* data);
void onMemberRemoved(const QString& chId, uint64_t nodeId);
private:
void scheduleBatch(int centerRow) const;
void loadBatch(int from, int count);
void evictDistant(int centerRow);
QPixmap makeAvatar(QColor color, QChar letter) const;
DbManager* m_db;
QString m_channelId;
int m_totalCount = 0;
QString m_channelId;
int m_totalCount = 0;
mutable QMap<int, MemberListItem> m_cache;
mutable int m_pendingFrom = -1;
mutable int m_pendingCount = 0;
QTimer* m_rttTimer;
};
#endif
+35 -60
View File
@@ -1,26 +1,33 @@
#include "memberpropsdialog.h"
#include "../db/db_manager.h"
#include "../transport/gui_bridge.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QMessageBox>
#include <cstring>
#include <openssl/evp.h>
#include "../../../lib/platform_compat.h"
extern "C" {
#include "../../../src/chat/chat_core.h"
#include "../../../src/chat/chat_member.h"
#include "../../../lib/mem.h"
}
MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
quint64 nodeId, const QString& memberName,
QWidget* parent)
uint64_t nodeId, const QString& memberName,
uint8_t flags, QWidget* parent)
: QDialog(parent)
, m_db(db)
, m_channelId(channelId)
, m_nodeId(nodeId)
, m_flags(flags)
{
setWindowTitle("Member Properties");
setMinimumWidth(340);
setModal(true);
/* channel private key (for enabling admin/super checkboxes) */
auto chs = m_db->getChannels();
for (const auto& ch : chs) {
if (ch.channelId == channelId) {
@@ -29,9 +36,12 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
}
}
bool haveKey = (m_edPrivkey.size() == 32);
bool isOwn = (m_nodeId == m_db->myNodeId());
bool isOwn = (m_nodeId == (uint64_t)m_db->myNodeId());
bool isAdmin = false, isSuper = false, isStorage = false;
bool isAdmin = (m_flags & CHAT_MEMBER_FLAG_ADMIN) != 0;
bool isSuper = (m_flags & CHAT_MEMBER_FLAG_SUPERNODE) != 0;
/* load current nick and version from DB (local_nick is local-only, ver is in adm_tags) */
QString curNick;
m_currentVer = 0;
if (m_db->m_db) {
@@ -45,24 +55,13 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
if (sqlite3_step(st) == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(st, 0);
if (tags) {
const char* vp = strstr(tags, "ver=");
if (vp) m_currentVer = atoi(vp + 4);
if (strstr(tags, "admin=yes")) isAdmin = true;
if (strstr(tags, "supernode=yes")) isSuper = true;
if (strstr(tags, "storage=yes")) isStorage = true;
const char* vp = strstr(tags, "\"ver\":\"");
if (vp) { vp += 7; m_currentVer = atoi(vp); }
}
curNick = DbManager::colText(st, 1);
}
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);
@@ -93,7 +92,6 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
layout->addWidget(m_superCb);
m_storageCb = new QCheckBox("Storage (priority attachment keeper)", this);
m_storageCb->setChecked(isStorage);
m_storageCb->setEnabled(haveKey);
layout->addWidget(m_storageCb);
@@ -134,10 +132,10 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
void MemberPropsDialog::onSave() {
if (!m_db->m_db) return;
QString tbl = m_db->peersTableName(m_channelId);
/* local_nick — always save (unsigned, local only) */
/* local_nick — always save local (unsigned, local only) */
QString nick = m_nickEdit->text().trimmed();
QString tbl = m_db->peersTableName(m_channelId);
QByteArray nickSql = QString(
"UPDATE \"%1\" SET local_nick=? WHERE node_id=?"
).arg(tbl).toUtf8();
@@ -150,49 +148,26 @@ void MemberPropsDialog::onSave() {
sqlite3_finalize(ns);
}
/* signed fields — only if key available */
/* signed fields — через chat_member_tags_commit_trampoline */
if (m_edPrivkey.size() != 32) { accept(); return; }
int ver = m_currentVer + 1;
QString tags = QString("ver=%1").arg(ver);
if (m_adminCb->isChecked()) tags += ",admin=yes";
if (m_superCb->isChecked()) tags += ",supernode=yes";
if (m_storageCb->isChecked()) tags += ",storage=yes";
QString json;
json += QString("{\"ver\":\"%1\"").arg(ver);
json += QString(",\"admin\":\"%1\"").arg(m_adminCb->isChecked() ? "yes" : "no");
json += QString(",\"supernode\":\"%1\"").arg(m_superCb->isChecked() ? "yes" : "no");
json += QString(",\"storage\":\"%1\"").arg(m_storageCb->isChecked() ? "yes" : "no");
json += "}";
QByteArray msg = tags.toUtf8();
uint64_t le = htole64(m_nodeId);
msg.append((const char*)&le, 8);
struct chat_member_tags_req* req = (struct chat_member_tags_req*)
u_calloc(1, sizeof(struct chat_member_tags_req));
if (!req) { accept(); return; }
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);
}
QByteArray sql = QString(
"UPDATE \"%1\" SET adm_tags=?, adm_tags_sig=?, node_type=?, storage=? 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);
int stg = m_storageCb->isChecked() ? 1 : 0;
sqlite3_bind_int(st, 4, stg);
sqlite3_bind_int64(st, 5, (sqlite3_int64)m_nodeId);
sqlite3_step(st);
sqlite3_finalize(st);
}
QByteArray ch_id_utf8 = m_channelId.toUtf8();
strncpy(req->ch_id, ch_id_utf8.constData(), sizeof(req->ch_id) - 1);
req->node_id = m_nodeId;
strncpy(req->json, json.toUtf8().constData(), sizeof(req->json) - 1);
gui_bridge_post_uasync_fn(chat_member_tags_commit_trampoline, req);
accept();
}
+5 -3
View File
@@ -4,6 +4,7 @@
#include <QCheckBox>
#include <QLineEdit>
#include <QString>
#include <stdint.h>
class DbManager;
@@ -11,8 +12,8 @@ class MemberPropsDialog : public QDialog {
Q_OBJECT
public:
explicit MemberPropsDialog(DbManager* db, const QString& channelId,
quint64 nodeId, const QString& memberName,
QWidget* parent = nullptr);
uint64_t nodeId, const QString& memberName,
uint8_t flags, QWidget* parent = nullptr);
private slots:
void onSave();
@@ -20,8 +21,9 @@ private slots:
private:
DbManager* m_db;
QString m_channelId;
quint64 m_nodeId;
uint64_t m_nodeId;
QByteArray m_edPrivkey;
uint8_t m_flags;
int m_currentVer;
QLineEdit* m_nickEdit;
+30 -4
View File
@@ -30,6 +30,16 @@ struct TOPO_GROUP_NODE;
#define GUI_EVT_NODEINFO_UPDATE 16 /* data: node_id:8 conn_presence:1 conn_up:1 best_rtt_packed:2 */
#define GUI_EVT_CONN_LIST 20 /* data: [count:2][entry:46B]* — binary conn list */
#define GUI_EVT_CONN_METRICS 21 /* data: text dump of selected connection metrics */
#define GUI_EVT_MEMBER_DETAIL 22 /* data: [node_id:8][flags:1][own_tcp_active:2][links...][socks...] — live snapshot for member detail panel */
#define GUI_EVT_MEMBER_LIST 23 /* data: [ch_id_len:1][ch_id:var][count:2][member:79B]* — полный список мемберов канала */
#define GUI_EVT_MEMBER_UPDATED 24 /* data: [ch_id_len:1][ch_id:var][count:1][member:79B] — обновление одного мембера */
#define GUI_EVT_MEMBER_REMOVED 25 /* data: [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */
/* Размеры элементов пакета GUI_EVT_MEMBER_DETAIL (должны совпадать с chat_status.c) */
#define MEMBER_DETAIL_LINK_SIZE 24
#define MEMBER_DETAIL_SOCK_SIZE 48
#define MEMBER_DETAIL_MAX_LINKS 16
#define MEMBER_DETAIL_MAX_SOCKS 16
/* ── API ── */
@@ -60,10 +70,6 @@ void gui_bridge_set_msg_received_cb(gui_msg_received_fn cb);
typedef void (*gui_channel_updated_fn)(const char* ch_id, int ch_id_len);
void gui_bridge_set_channel_updated_cb(gui_channel_updated_fn cb);
/* Callback для изменения списка участников (вызывается из GUI-потока) */
typedef void (*gui_members_changed_fn)(const char* ch_id, int ch_id_len);
void gui_bridge_set_members_changed_cb(gui_members_changed_fn cb);
/* Callback для получения своего node_id после инициализации uTun */
typedef void (*gui_my_node_id_fn)(uint64_t node_id);
void gui_bridge_set_my_node_id_cb(gui_my_node_id_fn cb);
@@ -110,6 +116,26 @@ void gui_bridge_set_conn_list_cb(gui_conn_list_fn cb);
typedef void (*gui_conn_metrics_fn)(const uint8_t* data, int len);
void gui_bridge_set_conn_metrics_cb(gui_conn_metrics_fn cb);
/* Callback для live-снапшота панели деталей участника (вызывается из GUI-потока)
data: [node_id:8][flags:1][own_tcp_active:2][links...][socks...] — см. chat_status.c collect_member_detail */
typedef void (*gui_member_detail_fn)(const uint8_t* data, int len);
void gui_bridge_set_member_detail_cb(gui_member_detail_fn cb);
/* Callback для получения полного списка мемберов канала (вызывается из GUI-потока)
data: [ch_id_len:1][ch_id:var][count:2][member:79B]* */
typedef void (*gui_member_list_fn)(const uint8_t* data, int len);
void gui_bridge_set_member_list_cb(gui_member_list_fn cb);
/* Callback для обновления одного мембера (вызывается из GUI-потока)
data: [ch_id_len:1][ch_id:var][count:1][member:79B] */
typedef void (*gui_member_updated_fn)(const uint8_t* data, int len);
void gui_bridge_set_member_updated_cb(gui_member_updated_fn cb);
/* Callback для удаления мембера из канала (вызывается из GUI-потока)
data: [ch_id_len:1][ch_id:var][node_id:8] */
typedef void (*gui_member_removed_fn)(const uint8_t* data, int len);
void gui_bridge_set_member_removed_cb(gui_member_removed_fn cb);
/* uTun nodeinfo callback — registered via utun_add_nodeinfo_cbk, called from uasync thread */
void gui_nodeinfo_cb_impl(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq, void* arg);
+32 -14
View File
@@ -29,7 +29,6 @@ static GuiBridgeReceiver* g_receiver = nullptr;
static gui_connect_result_fn g_connect_result_cb = nullptr;
static gui_msg_received_fn g_msg_received_cb = nullptr;
static gui_channel_updated_fn g_channel_updated_cb = nullptr;
static gui_members_changed_fn g_members_changed_cb = nullptr;
static gui_my_node_id_fn g_my_node_id_cb = nullptr;
static gui_auto_connect_status_fn g_auto_connect_status_cb = nullptr;
static gui_channel_peers_online_fn g_channel_peers_online_cb = nullptr;
@@ -41,6 +40,10 @@ static gui_nodeinfo_update_fn g_nodeinfo_update_cb = nullptr;
static gui_invite_received_fn g_invite_received_cb = nullptr;
static gui_conn_list_fn g_conn_list_cb = nullptr;
static gui_conn_metrics_fn g_conn_metrics_cb = nullptr;
static gui_member_detail_fn g_member_detail_cb = nullptr;
static gui_member_list_fn g_member_list_cb = nullptr;
static gui_member_updated_fn g_member_updated_cb = nullptr;
static gui_member_removed_fn g_member_removed_cb = nullptr;
static struct UASYNC* g_ua = nullptr;
/* ── GuiBridgeReceiver implementation ── */
@@ -98,15 +101,6 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: CHANNEL_UPDATED data too short %d", dlen);
}
break;
case GUI_EVT_MEMBERS_CHANGED:
if (dlen >= 2) {
uint8_t chLen = d[0];
if (dlen >= 1 + chLen) {
QString chId = QString::fromUtf8((const char*)d + 1, chLen);
if (g_members_changed_cb) g_members_changed_cb((const char*)d + 1, chLen);
}
}
break;
case GUI_EVT_MY_NODE_ID:
if (dlen >= 8) {
uint64_t nid; memcpy(&nid, d, 8);
@@ -188,6 +182,18 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
case GUI_EVT_CONN_METRICS:
if (g_conn_metrics_cb) g_conn_metrics_cb(d, dlen);
break;
case GUI_EVT_MEMBER_DETAIL:
if (g_member_detail_cb) g_member_detail_cb(d, dlen);
break;
case GUI_EVT_MEMBER_LIST:
if (g_member_list_cb) g_member_list_cb(d, dlen);
break;
case GUI_EVT_MEMBER_UPDATED:
if (g_member_updated_cb) g_member_updated_cb(d, dlen);
break;
case GUI_EVT_MEMBER_REMOVED:
if (g_member_removed_cb) g_member_removed_cb(d, dlen);
break;
default:
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType);
break;
@@ -245,10 +251,6 @@ void gui_bridge_set_channel_updated_cb(gui_channel_updated_fn cb) {
g_channel_updated_cb = cb;
}
void gui_bridge_set_members_changed_cb(gui_members_changed_fn cb) {
g_members_changed_cb = cb;
}
void gui_bridge_set_my_node_id_cb(gui_my_node_id_fn cb) {
g_my_node_id_cb = cb;
}
@@ -293,6 +295,22 @@ void gui_bridge_set_conn_metrics_cb(gui_conn_metrics_fn cb) {
g_conn_metrics_cb = cb;
}
void gui_bridge_set_member_detail_cb(gui_member_detail_fn cb) {
g_member_detail_cb = cb;
}
void gui_bridge_set_member_list_cb(gui_member_list_fn cb) {
g_member_list_cb = cb;
}
void gui_bridge_set_member_updated_cb(gui_member_updated_fn cb) {
g_member_updated_cb = cb;
}
void gui_bridge_set_member_removed_cb(gui_member_removed_fn cb) {
g_member_removed_cb = cb;
}
} /* extern "C" */
/* ── uTun nodeinfo callback (called from uasync thread) ── */
+8 -8
View File
@@ -101,15 +101,9 @@ void UtunNode::stop() {
}
void UtunNode::finalize() {
if (!m_instance) return;
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "finalize: cleanup start");
etcp_router_bind(m_instance, ETCP_RT_ID_CHAT, nullptr);
chat_sync_destroy(m_instance);
chat_core_destroy(m_instance);
utun_instance_destroy(m_instance);
if (m_ua) { uasync_destroy(m_ua, 0); m_ua = nullptr; }
m_instance = nullptr;
uasync_destroy(m_ua, 0);
m_ua = nullptr;
m_running = false;
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "finalize: cleanup done");
}
@@ -409,6 +403,12 @@ void UtunNode::runLoop() {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: poll exit m_stop=%d", (int)m_stop);
if (m_instance) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: destroying instance in worker thread");
utun_instance_destroy(m_instance);
m_instance = nullptr;
}
signal(SIGINT, SIG_DFL);
signal(SIGTERM, SIG_DFL);