You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

446 lines
16 KiB

#include "accountlist.h"
#include "accountdelegate.h"
#include "memberlistmodel.h"
#include "memberpropsdialog.h"
#include "renamedialog.h"
#include "../db/db_manager.h"
#include "../transport/gui_bridge.h"
#include <QVBoxLayout>
#include <QSplitter>
#include <QLabel>
#include <QDateTime>
#include <QFont>
#include <QSet>
#include <algorithm>
#include <cstring>
#include "../../../lib/platform_compat.h"
extern "C" {
#include "../lib/socket_compat.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) {
if (family == 4) {
const uint8_t* a = (const uint8_t*)addr;
return QString("%1.%2.%3.%4").arg(a[0]).arg(a[1]).arg(a[2]).arg(a[3]);
}
if (family == 6) {
const uint16_t* a = (const uint16_t*)addr;
return QString("%1:%2:%3:%4:%5:%6:%7:%8")
.arg(ntohs(a[0]), 0, 16).arg(ntohs(a[1]), 0, 16)
.arg(ntohs(a[2]), 0, 16).arg(ntohs(a[3]), 0, 16)
.arg(ntohs(a[4]), 0, 16).arg(ntohs(a[5]), 0, 16)
.arg(ntohs(a[6]), 0, 16).arg(ntohs(a[7]), 0, 16);
}
return "?";
}
QString AccountList::relativeTimeStr(qint64 timestamp) {
if (timestamp <= 0) return "";
qint64 diff = QDateTime::currentSecsSinceEpoch() - timestamp;
if (diff < 0) diff = 0;
if (diff < 60) return QString("%1s").arg(diff);
if (diff < 3600) return QString("%1m").arg(diff / 60);
if (diff < 86400) return QString("%1h").arg(diff / 3600);
return QString("%1d").arg(diff / 86400);
}
AccountList::AccountList(DbManager* db, QWidget *parent)
: QWidget(parent)
, m_listView(new QListView(this))
, m_model(new MemberListModel(this))
, m_db(db)
, m_detailText(nullptr)
{
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
auto *header = new QLabel("Members", this);
header->setAlignment(Qt::AlignCenter);
header->setStyleSheet("font-weight: bold; padding: 8px; background: palette(window);");
layout->addWidget(header);
m_listView->setModel(m_model);
m_listView->setItemDelegate(new AccountDelegate(m_listView));
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_listView->setSelectionMode(QAbstractItemView::SingleSelection);
m_listView->setIconSize(QSize(24, 24));
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
m_listView->setContextMenuPolicy(Qt::CustomContextMenu);
m_detailText = new QTextEdit(this);
m_detailText->setReadOnly(true);
m_detailText->setFont(QFont("monospace", 10));
m_detailText->setStyleSheet(
"QTextEdit { border-top: 1px solid palette(mid); border-bottom: none;"
" border-left: none; border-right: none; padding: 6px; background: palette(base); }");
m_detailText->setPlaceholderText("Select a member");
auto *memberSplitter = new QSplitter(Qt::Vertical, this);
memberSplitter->addWidget(m_listView);
memberSplitter->addWidget(m_detailText);
memberSplitter->setStretchFactor(0, 3);
memberSplitter->setStretchFactor(1, 2);
memberSplitter->setSizes({360, 240});
layout->addWidget(memberSplitter, 1);
connect(m_listView->selectionModel(), &QItemSelectionModel::selectionChanged,
m_listView->viewport(), qOverload<>(&QWidget::update));
connect(m_listView, &QListView::clicked, this, &AccountList::onMemberClicked);
connect(m_listView, &QListView::doubleClicked, this, &AccountList::onMemberDoubleClicked);
connect(m_listView, &QWidget::customContextMenuRequested,
this, &AccountList::onMemberContextMenu);
m_detailTimer = new QTimer(this);
m_detailTimer->setInterval(5000);
connect(m_detailTimer, &QTimer::timeout, this, &AccountList::onDetailTimerTick);
m_linkTimer = new QTimer(this);
m_linkTimer->setInterval(1000);
connect(m_linkTimer, &QTimer::timeout, this, [this]{
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) {
m_channelId = channelId;
m_model->setChannel(channelId);
m_detailTimer->stop();
m_linkTimer->stop();
m_detailNodeId = 0;
m_detailText->clear();
}
void AccountList::refresh() { m_model->refresh(); }
void AccountList::onMemberClicked(const QModelIndex& index) {
if (!index.isValid()) return;
quint64 nodeId = m_model->nodeIdAt(index.row());
if (nodeId) showMemberDetail(nodeId);
}
void AccountList::showMemberDetail(quint64 nodeId) {
if (!m_db || !m_db->m_db) return;
m_detailTimer->stop();
m_linkTimer->stop();
m_detailNodeId = nodeId;
m_detailName.clear();
m_detailOnline = false;
m_detailLastSeen = 0;
m_detailCreated = 0;
m_detailAddrs.clear();
m_snapConnPresent = false;
m_snapConnUp = false;
m_snapConnInit = false;
m_snapClient = false;
m_snapServer = false;
m_snapOwnTcpActive = 0;
m_snapLinks.clear();
m_snapSocks.clear();
sqlite3* d = m_db->m_db;
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(d,
"SELECT name, online, last_seen_at, created_at FROM nodes WHERE node_id=?",
-1, &st, nullptr) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId);
if (sqlite3_step(st) == SQLITE_ROW) {
m_detailName = DbManager::colText(st, 0);
m_detailOnline = sqlite3_column_int(st, 1) != 0;
m_detailLastSeen = sqlite3_column_int64(st, 2);
m_detailCreated = sqlite3_column_int64(st, 3);
}
sqlite3_finalize(st);
}
if (m_detailName.isEmpty())
m_detailName = QString("0x%1").arg(nodeId, 8, 16, QChar('0')).right(4).toUpper();
/* prefer local nickname */
QString tbl = m_db->peersTableName(m_channelId);
sqlite3_stmt* nickSt = nullptr;
QByteArray nickSql = QString(
"SELECT local_nick FROM \"%1\" WHERE node_id=?"
).arg(tbl).toUtf8();
if (sqlite3_prepare_v2(d, nickSql.constData(), -1, &nickSt, nullptr) == SQLITE_OK) {
sqlite3_bind_int64(nickSt, 1, (sqlite3_int64)nodeId);
if (sqlite3_step(nickSt) == SQLITE_ROW) {
QString n = DbManager::colText(nickSt, 0);
if (!n.isEmpty()) m_detailName = n;
}
sqlite3_finalize(nickSt);
}
sqlite3_stmt* na = nullptr;
sqlite3_prepare_v2(d,
"SELECT family, protocol, address, port, socket_id FROM node_addresses WHERE node_id=?",
-1, &na, nullptr);
if (na) {
sqlite3_bind_int64(na, 1, (sqlite3_int64)nodeId);
while (sqlite3_step(na) == SQLITE_ROW) {
DetailAddress a;
a.family = sqlite3_column_int(na, 0);
a.protocol = sqlite3_column_int(na, 1);
const void* adr = sqlite3_column_blob(na, 2);
int sz = sqlite3_column_bytes(na, 2);
if (adr && sz > 0) a.addr = QByteArray((const char*)adr, sz);
a.port = sqlite3_column_int(na, 3);
a.socket_id = sqlite3_column_int(na, 4);
if (!a.addr.isEmpty()) m_detailAddrs.append(a);
}
sqlite3_finalize(na);
}
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;
});
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;
m_snapClient = (flags & 8) != 0;
m_snapServer = (flags & 16) != 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() {
QStringList lines;
lines.append(QString("ID:0x%1").arg(m_detailNodeId, 16, 16, QChar('0')));
QString dotColor = m_detailOnline ? "#4CAF50" : "#9E9E9E";
QString scLabel;
if (m_snapClient) scLabel = " C";
else if (m_snapServer) scLabel = " S";
lines.append(QString("<span style='color:%1;'>●</span> %2%3")
.arg(dotColor, m_detailName.toHtmlEscaped(), scLabel));
if (m_detailLastSeen > 0)
lines.append(QString("⌛ %1").arg(relativeTimeStr(m_detailLastSeen)));
if (m_detailCreated > 0)
lines.append(QString("📅 %1").arg(relativeTimeStr(m_detailCreated)));
bool isSelf = m_db ? (m_detailNodeId == m_db->myNodeId()) : false;
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 (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 (const auto& lk : m_snapLinks) {
if (lk.isTcp && lk.tcpReady) { linkColor = "#4CAF50"; break; }
}
}
}
QString prefix;
if (isSelf) {
int linkCount = 0;
if (a.protocol == 1) {
for (const auto& s : m_snapSocks) {
if (s.port == a.port) { linkCount = s.linkCount; break; }
}
} else if (a.protocol == 2) {
linkCount = m_snapOwnTcpActive;
}
if (linkCount > 0) prefix = QString("[%1] ").arg(linkCount);
}
if (!a.ifname.isEmpty()) {
lines.append(QString("%1<span style='color:%2;'>●</span> %3 %4")
.arg(prefix, linkColor, proto, a.ifname));
lines.append(QString("&nbsp;&nbsp;%1:<span style='color:#FF9800;'>%2</span>")
.arg(addrStr(a.addr.constData(), a.family)).arg(a.port));
} else {
lines.append(QString("%1<span style='color:%2;'>●</span> %3:%4:%5")
.arg(prefix, linkColor, proto, addrStr(a.addr.constData(), a.family)).arg(a.port));
}
}
/* 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, addrStr(lk.addr.constData(), lk.family)).arg(lk.port));
}
}
m_detailText->setHtml(lines.join("<br>"));
}
void AccountList::onDetailTimerTick() {
if (m_detailNodeId != 0) requestMemberDetail();
}
void AccountList::onMemberDoubleClicked(const QModelIndex& index) {
if (!index.isValid()) return;
quint64 nodeId = m_model->nodeIdAt(index.row());
if (!nodeId) return;
QString name = m_model->data(index, Qt::DisplayRole).toString();
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::onMemberContextMenu(const QPoint& pos) {
QModelIndex index = m_listView->indexAt(pos);
if (!index.isValid()) return;
quint64 nodeId = m_model->nodeIdAt(index.row());
if (!nodeId) return;
QString name = m_model->data(index, Qt::DisplayRole).toString();
QMenu menu(m_listView);
QAction* renameAction = menu.addAction("Rename");
if (nodeId == m_db->myNodeId()) renameAction->setEnabled(false);
QAction* editAction = menu.addAction("Edit");
QAction* chosen = menu.exec(m_listView->viewport()->mapToGlobal(pos));
if (chosen == renameAction) {
RenameDialog dlg(m_db, m_channelId, nodeId, name, this);
if (dlg.exec() == QDialog::Accepted) {
m_model->refresh();
emit memberRenamed();
}
return;
}
if (chosen != editAction) return;
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);
}