etcp: переупорядочить close + защита state==2 во всех API-точках; topo: правильная очистка group nodes и registry; chat/member: новый модуль member_sync; chatgui: обновления транспорта и UI
This commit is contained in:
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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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*/,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,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);
|
||||
|
||||
|
||||
@@ -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) ── */
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user