Browse Source

chat: deleted-аккаунты — колонка deleted, скрытие/зачёркивание, не подключаемся

v2
evgeny 4 weeks ago
parent
commit
a17a85a383
  1. 6
      src/chat/chat_member.c
  2. 1
      src/chat/chat_member.h
  3. 74
      src/routing_layer/topo_node_sqlite.c
  4. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt
  5. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt
  6. 11
      tools/chatgui/src/accountdelegate.cpp
  7. 14
      tools/chatgui/src/accountlist.cpp
  8. 1
      tools/chatgui/src/flagpainter.h
  9. 23
      tools/chatgui/src/memberlistmodel.cpp
  10. 3
      tools/chatgui/src/memberlistmodel.h

6
src/chat/chat_member.c

@ -108,7 +108,7 @@ int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
snprintf(sql, sizeof(sql),
"SELECT p.node_id, COALESCE(n.name,''),"
" COALESCE(p.local_nick,''), COALESCE(p.adm_tags,''),"
" COALESCE(p.storage,0)"
" COALESCE(p.storage,0), COALESCE(p.deleted,0)"
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
" ORDER BY p.node_id ASC", peers_tbl);
@ -137,6 +137,7 @@ int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
m->storage = (uint8_t)sqlite3_column_int(st, 4);
m->is_self = (uint8_t)(m->node_id == my_id ? 1 : 0);
m->flags = parse_adm_tags_flags(adm_tags);
if (sqlite3_column_int(st, 5)) m->flags |= CHAT_MEMBER_FLAG_DELETED;
m->rtt = 0xFFFF;
m->nat_type = 0xFF;
@ -228,7 +229,7 @@ int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* ou
snprintf(sql, sizeof(sql),
"SELECT COALESCE(n.name,''),"
" COALESCE(p.local_nick,''), COALESCE(p.adm_tags,''),"
" COALESCE(p.storage,0)"
" COALESCE(p.storage,0), COALESCE(p.deleted,0)"
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
" WHERE p.node_id=?", peers_tbl);
@ -245,6 +246,7 @@ int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* ou
m.storage = (uint8_t)sqlite3_column_int(st, 3);
m.is_self = (uint8_t)(node_id == my_id ? 1 : 0);
m.flags = parse_adm_tags_flags(adm_tags);
if (sqlite3_column_int(st, 4)) m.flags |= CHAT_MEMBER_FLAG_DELETED;
m.rtt = 0xFFFF;
m.nat_type = 0xFF;

1
src/chat/chat_member.h

@ -29,6 +29,7 @@ extern "C" {
#define CHAT_MEMBER_FLAG_SUPERNODE 0x01
#define CHAT_MEMBER_FLAG_ADMIN 0x02
#define CHAT_MEMBER_FLAG_MODER 0x04
#define CHAT_MEMBER_FLAG_DELETED 0x08
#define CHAT_MEMBER_DISPLAY_SIZE 79

74
src/routing_layer/topo_node_sqlite.c

@ -32,6 +32,12 @@ static void peers_table_name(const char* ch_id, char* buf, size_t sz) {
snprintf(buf, sz, "peers_%s", san);
}
static int adm_tags_deleted(const char* tags) {
if (!tags || !tags[0]) return 0;
char buf[16];
return json_flat_get(tags, "deleted", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0;
}
/* ── Классификация адресов (локально, по IP) ── */
static int addr_type_from_ip(int family, const uint8_t* addr) {
@ -292,6 +298,9 @@ int topo_node_sqlite_init(sqlite3* db) {
snprintf(isql, sizeof(isql),
"ALTER TABLE \"%s\" ADD COLUMN source INTEGER NOT NULL DEFAULT 0", tbl);
sqlite3_exec(db, isql, NULL, NULL, NULL);
snprintf(isql, sizeof(isql),
"ALTER TABLE \"%s\" ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0", tbl);
sqlite3_exec(db, isql, NULL, NULL, NULL);
}
sqlite3_finalize(s);
}
@ -338,6 +347,7 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
" node_RTT INTEGER," /* RTT до узла в мс, часть индекса (node_type, node_RTT) */
" connected INTEGER NOT NULL DEFAULT 0," /* 1 = было активное подключение к узлу */
" storage INTEGER NOT NULL DEFAULT 0," /* 1 = приоритетный хранитель аттачей */
" deleted INTEGER NOT NULL DEFAULT 0," /* 1 = аккаунт удалён владельцем (не отображаем, не подключаемся) */
" local_nick TEXT," /* локальный ник (GUI only, не синхронизируется) */
" source INTEGER NOT NULL DEFAULT 0" /* 0=полная merkle-запись 1=упрощённый плейсхолдер topo_group
(не участвует в merkle-хеше, дополняется полной записью) */
@ -424,7 +434,7 @@ int topo_node_sqlite_member_put(sqlite3* db, const char* channel_id, uint64_t no
if (adm_tags || adm_tags_sig || storage > 0) {
snprintf(sql, sizeof(sql),
"UPDATE \"%s\" SET adm_tags=?, adm_tags_sig=?, storage=? WHERE node_id=?", peers_tbl);
"UPDATE \"%s\" SET adm_tags=?, adm_tags_sig=?, storage=?, deleted=? WHERE node_id=?", peers_tbl);
sqlite3_stmt* u = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) {
if (adm_tags && adm_tags[0]) sqlite3_bind_text(u, 1, adm_tags, -1, SQLITE_STATIC);
@ -432,7 +442,8 @@ int topo_node_sqlite_member_put(sqlite3* db, const char* channel_id, uint64_t no
if (adm_tags_sig) sqlite3_bind_blob(u, 2, adm_tags_sig, 64, SQLITE_STATIC);
else sqlite3_bind_null(u, 2);
sqlite3_bind_int(u, 3, storage);
sqlite3_bind_int64(u, 4, (sqlite3_int64)node_id);
sqlite3_bind_int(u, 4, adm_tags_deleted(adm_tags));
sqlite3_bind_int64(u, 5, (sqlite3_int64)node_id);
sqlite3_step(u); sqlite3_finalize(u);
}
}
@ -512,7 +523,7 @@ int topo_node_sqlite_member_owner_put(sqlite3* db, const char* channel_id, uint6
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256];
snprintf(sql, sizeof(sql),
"UPDATE \"%s\" SET adm_tags=?, adm_tags_sig=?, storage=? WHERE node_id=?", peers_tbl);
"UPDATE \"%s\" SET adm_tags=?, adm_tags_sig=?, storage=?, deleted=? WHERE node_id=?", peers_tbl);
sqlite3_stmt* u = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) != SQLITE_OK) return -1;
if (adm_tags && adm_tags[0]) sqlite3_bind_text(u, 1, adm_tags, -1, SQLITE_STATIC);
@ -520,7 +531,8 @@ int topo_node_sqlite_member_owner_put(sqlite3* db, const char* channel_id, uint6
if (adm_tags_sig) sqlite3_bind_blob(u, 2, adm_tags_sig, 64, SQLITE_STATIC);
else sqlite3_bind_null(u, 2);
sqlite3_bind_int(u, 3, storage);
sqlite3_bind_int64(u, 4, (sqlite3_int64)node_id);
sqlite3_bind_int(u, 4, adm_tags_deleted(adm_tags));
sqlite3_bind_int64(u, 5, (sqlite3_int64)node_id);
sqlite3_step(u); sqlite3_finalize(u);
return 0;
}
@ -785,32 +797,34 @@ int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id) {
while (sqlite3_step(s) == SQLITE_ROW) {
const char* tbl = (const char*)sqlite3_column_text(s, 0);
int node_type = base_type; int is_storage = 0;
if (best_at == ADDR_TYPE_DIRECT && base_type == 1) {
snprintf(sql, sizeof(sql),
"SELECT adm_tags FROM \"%s\" WHERE node_id=?", tbl);
sqlite3_stmt* a = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &a, NULL) == SQLITE_OK) {
sqlite3_bind_int64(a, 1, (sqlite3_int64)node_id);
if (sqlite3_step(a) == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(a, 0);
if (tags) {
char buf[64];
int node_type = base_type; int is_storage = 0; int is_deleted = 0;
snprintf(sql, sizeof(sql),
"SELECT adm_tags FROM \"%s\" WHERE node_id=?", tbl);
sqlite3_stmt* a = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &a, NULL) == SQLITE_OK) {
sqlite3_bind_int64(a, 1, (sqlite3_int64)node_id);
if (sqlite3_step(a) == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(a, 0);
if (tags) {
char buf[64];
if (best_at == ADDR_TYPE_DIRECT && base_type == 1) {
if (json_flat_get(tags, "supernode", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) node_type = 4;
if (json_flat_get(tags, "storage", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_storage = 1;
}
is_deleted = adm_tags_deleted(tags);
}
sqlite3_finalize(a);
}
sqlite3_finalize(a);
}
snprintf(sql, sizeof(sql),
"UPDATE \"%s\" SET node_type=?, storage=? WHERE node_id=?", tbl);
"UPDATE \"%s\" SET node_type=?, storage=?, deleted=? WHERE node_id=?", tbl);
sqlite3_stmt* u = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) {
sqlite3_bind_int(u, 1, node_type);
sqlite3_bind_int(u, 2, is_storage);
sqlite3_bind_int64(u, 3, (sqlite3_int64)node_id);
sqlite3_bind_int(u, 3, is_deleted);
sqlite3_bind_int64(u, 4, (sqlite3_int64)node_id);
sqlite3_step(u); sqlite3_finalize(u);
}
}
@ -1013,7 +1027,7 @@ int topo_node_sqlite_get_connected_peers(sqlite3* db, const char* channel_id,
if (!db || !channel_id || !out_ids || !out_count) return -1;
*out_ids = NULL; *out_count = 0;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[128]; snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" WHERE connected=1", peers_tbl);
char sql[192]; snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" WHERE connected=1 AND deleted=0", peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
int cnt = 0;
@ -1030,13 +1044,15 @@ int topo_node_sqlite_get_connected_peers(sqlite3* db, const char* channel_id,
int topo_node_sqlite_get_public_peers(sqlite3* db, const char* channel_id,
uint64_t** out_ids, int* out_count, uint64_t self_node_id) {
if (!db || !out_ids || !out_count) return -1;
if (!db || !channel_id || !out_ids || !out_count) return -1;
*out_ids = NULL; *out_count = 0;
(void)channel_id;
const char* sql =
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[384]; snprintf(sql, sizeof(sql),
"SELECT DISTINCT na.node_id FROM node_addresses na"
" WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (1,2)"
" AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)";
" AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)"
" AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)",
peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id);
@ -1058,7 +1074,7 @@ int topo_node_sqlite_get_supernode_peers(sqlite3* db, const char* channel_id,
*out_ids = NULL; *out_count = 0;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256]; snprintf(sql, sizeof(sql),
"SELECT p.node_id FROM \"%s\" p WHERE p.node_type=4"
"SELECT p.node_id FROM \"%s\" p WHERE p.node_type=4 AND p.deleted=0"
" AND EXISTS (SELECT 1 FROM node_addresses na WHERE na.node_id=p.node_id AND na.family IN (4,6))",
peers_tbl);
sqlite3_stmt* st = NULL;
@ -1077,13 +1093,15 @@ int topo_node_sqlite_get_supernode_peers(sqlite3* db, const char* channel_id,
int topo_node_sqlite_get_local_peers(sqlite3* db, const char* channel_id,
uint64_t** out_ids, int* out_count, uint64_t self_node_id) {
if (!db || !out_ids || !out_count) return -1;
if (!db || !channel_id || !out_ids || !out_count) return -1;
*out_ids = NULL; *out_count = 0;
(void)channel_id;
const char* sql =
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[384]; snprintf(sql, sizeof(sql),
"SELECT DISTINCT na.node_id FROM node_addresses na"
" WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (0,3)"
" AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)";
" AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)"
" AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)",
peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id);

2
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt

@ -24,6 +24,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -176,6 +177,7 @@ private fun MemberListItem(
fontSize = 15.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textDecoration = if (member.isDeleted) TextDecoration.LineThrough else TextDecoration.None,
color = if (member.isSelf) Color(0xFF4CAF50) else Color.Unspecified
)
val parts = mutableListOf<String>()

2
tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt

@ -382,7 +382,7 @@ class ChatViewModel : ViewModel() {
viewModelScope.launch {
val (raw, owner) = withContext(Dispatchers.IO) { r.getMembers(chId) to r.isChannelOwner(chId) }
_channelOwner.value = owner
_members.value = raw.map { m ->
_members.value = raw.filter { owner || !it.isDeleted }.map { m ->
val ns = statuses[m.nodeId]
val base = if (ns != null) m.copy(
online = ns.online, rtt = ns.bestRtt,

11
tools/chatgui/src/accountdelegate.cpp

@ -29,9 +29,12 @@ void AccountDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
const int w = option.rect.right() - x - margin;
const int yMid = option.rect.top() + option.rect.height() / 2;
uint8_t flags = (uint8_t)(index.data(Qt::UserRole + 3).toInt() & 0xFF);
QFont nameFont = painter->font();
nameFont.setBold(true);
nameFont.setPixelSize(13);
if (flags & MEMBER_FLAG_DELETED) nameFont.setStrikeOut(true);
painter->setFont(nameFont);
QColor textColor = option.palette.color(QPalette::Text);
@ -39,12 +42,12 @@ void AccountDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
if (index.data(Qt::UserRole + 2).toBool()) textColor = QColor("#4CAF50");
painter->setPen(textColor);
uint8_t flags = (uint8_t)(index.data(Qt::UserRole + 3).toInt() & 0xFF);
const uint8_t visualFlags = flags & (MEMBER_FLAG_SUPERNODE | MEMBER_FLAG_ADMIN | MEMBER_FLAG_MODER);
int flagX = x;
if (flags) {
if (visualFlags) {
FlagPainter::draw(painter, flags, flagX, option.rect.top() + (option.rect.height() - nameFont.pixelSize()) / 2, nameFont.pixelSize());
flagX += nameFont.pixelSize() * ((flags & 0x01 ? 1 : 0) + (flags & 0x02 ? 1 : 0) + (flags & 0x04 ? 1 : 0))
+ ((__builtin_popcount(flags) - 1) * 2);
int n = __builtin_popcount(visualFlags);
flagX += nameFont.pixelSize() * n + (n - 1) * 2;
}
QString name = index.data(Qt::DisplayRole).toString();

14
tools/chatgui/src/accountlist.cpp

@ -149,12 +149,6 @@ AccountList::~AccountList() {
void AccountList::setChannel(const QString& channelId) {
m_channelId = channelId;
m_model->setChannel(channelId);
m_detailTimer->stop();
m_linkTimer->stop();
m_detailNodeId = 0;
m_detailHeader->setText("Select a member");
m_detailText->clear();
bool isOwner = false;
if (m_db && m_db->isOpen()) {
@ -163,6 +157,14 @@ void AccountList::setChannel(const QString& channelId) {
if (ch.channelId == channelId) { isOwner = (ch.ed25519_privkey.size() == 32); break; }
}
}
m_model->setChannel(channelId, isOwner);
m_detailTimer->stop();
m_linkTimer->stop();
m_detailNodeId = 0;
m_detailHeader->setText("Select a member");
m_detailText->clear();
m_header->setText(isOwner ? QString::fromUtf8("Members \xF0\x9F\x94\x91") : QStringLiteral("Members"));
m_header->setToolTip(isOwner ? QStringLiteral("Channel owner (has private key)") : QString());
}

1
tools/chatgui/src/flagpainter.h

@ -8,6 +8,7 @@
#define MEMBER_FLAG_SUPERNODE 0x01
#define MEMBER_FLAG_ADMIN 0x02
#define MEMBER_FLAG_MODER 0x04
#define MEMBER_FLAG_DELETED 0x08
class FlagPainter {
public:

23
tools/chatgui/src/memberlistmodel.cpp

@ -56,9 +56,10 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const {
return {};
}
void MemberListModel::setChannel(const QString& channelId) {
void MemberListModel::setChannel(const QString& channelId, bool isOwner) {
beginResetModel();
m_channelId = channelId;
m_isOwner = isOwner;
m_totalCount = 0;
m_cache.clear();
endResetModel();
@ -70,7 +71,7 @@ void MemberListModel::setChannel(const QString& channelId) {
}
void MemberListModel::refresh() {
if (!m_channelId.isEmpty()) setChannel(m_channelId);
if (!m_channelId.isEmpty()) setChannel(m_channelId, m_isOwner);
}
void MemberListModel::refreshRtt() {
@ -119,14 +120,17 @@ void MemberListModel::onMemberList(const QString& chId, int count, const uint8_t
if (chId != m_channelId) return;
beginResetModel();
m_totalCount = count;
m_totalCount = 0;
m_cache.clear();
int row = 0;
for (int i = 0; i < count; i++) {
MemberListItem item;
deserialize_member(data + i * CHAT_MEMBER_DISPLAY_SIZE, item);
m_cache[i] = item;
if ((item.flags & CHAT_MEMBER_FLAG_DELETED) && !m_isOwner) continue;
m_cache[row++] = item;
}
m_totalCount = row;
endResetModel();
}
@ -142,6 +146,17 @@ void MemberListModel::onMemberUpdated(const QString& chId, const uint8_t* data)
if (it->nodeId == item.nodeId) { row = it.key(); break; }
}
if ((item.flags & CHAT_MEMBER_FLAG_DELETED) && !m_isOwner) {
/* deleted member — remove from non-owner's list */
if (row >= 0) {
beginRemoveRows(QModelIndex(), row, row);
m_cache.remove(row);
m_totalCount--;
endRemoveRows();
}
return;
}
if (row >= 0) {
m_cache[row] = item;
emit dataChanged(index(row), index(row));

3
tools/chatgui/src/memberlistmodel.h

@ -29,7 +29,7 @@ public:
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
void setChannel(const QString& channelId);
void setChannel(const QString& channelId, bool isOwner);
void refresh();
void refreshRtt();
quint64 nodeIdAt(int row) const;
@ -44,6 +44,7 @@ private:
QPixmap makeAvatar(QColor color, QChar letter) const;
QString m_channelId;
bool m_isOwner = false;
int m_totalCount = 0;
mutable QMap<int, MemberListItem> m_cache;
QTimer* m_rttTimer;

Loading…
Cancel
Save