diff --git a/tools/chatgui/src/memberpropsdialog.cpp b/tools/chatgui/src/memberpropsdialog.cpp index 67568845..068ff407 100644 --- a/tools/chatgui/src/memberpropsdialog.cpp +++ b/tools/chatgui/src/memberpropsdialog.cpp @@ -1,18 +1,19 @@ #include #include "memberpropsdialog.h" #include "../db/db_manager.h" -#include "../transport/gui_bridge.h" +#include "../transport/member_edit.h" +#include "debug_config.h" #include #include #include #include #include -#include +#include +#include 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, @@ -31,15 +32,15 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, setModal(true); /* channel private key (for enabling admin/super checkboxes) */ - auto chs = m_db->getChannels(); + auto chs = m_db ? m_db->getChannels() : QList(); for (const auto& ch : chs) { if (ch.channelId == channelId) { - m_edPrivkey = ch.ed25519_privkey; + m_haveAdminKey = ch.ed25519_privkey.size() == 32; break; } } - bool haveKey = (m_edPrivkey.size() == 32); - bool isOwn = (m_nodeId == (uint64_t)m_db->myNodeId()); + bool haveKey = m_haveAdminKey; + bool isOwn = m_isOwn = m_db && m_nodeId == (uint64_t)m_db->myNodeId(); bool isAdmin = (m_flags & CHAT_MEMBER_FLAG_ADMIN) != 0; bool isSuper = (m_flags & CHAT_MEMBER_FLAG_SUPERNODE) != 0; @@ -50,30 +51,29 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, bool curDeleted = false; bool curStorage = false; m_currentVer = 0; - if (m_db->m_db) { + bool loaded = false; + if (m_db && m_db->m_db) { QString tbl = m_db->peersTableName(channelId); QByteArray sql = QString( "SELECT adm_tags, local_nick FROM \"%1\" WHERE node_id=?" ).arg(tbl).toUtf8(); sqlite3_stmt* st = nullptr; - if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &st, nullptr) == SQLITE_OK) { - sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId); - if (sqlite3_step(st) == SQLITE_ROW) { - const char* tags = (const char*)sqlite3_column_text(st, 0); - if (tags) { - const char* vp = strstr(tags, "\"ver\":\""); - if (vp) { vp += 7; m_currentVer = atoi(vp); } - const char* vr = strstr(tags, "\"verified\":\""); - if (vr) { vr += 12; curVerified = (strncmp(vr, "yes", 3) == 0); } - const char* dl = strstr(tags, "\"deleted\":\""); - if (dl) { dl += 11; curDeleted = (strncmp(dl, "yes", 3) == 0); } - const char* sg = strstr(tags, "\"storage\":\""); - if (sg) { sg += 11; curStorage = (strncmp(sg, "yes", 3) == 0); } - } - curNick = DbManager::colText(st, 1); - } - sqlite3_finalize(st); + int rc = sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &st, nullptr); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc == SQLITE_ROW) { + const QJsonObject tags = QJsonDocument::fromJson(DbManager::colText(st, 0).toUtf8()).object(); + m_currentVer = tags.value("ver").toString().toInt(); + curVerified = tags.value("verified").toString() == "yes"; + curDeleted = tags.value("deleted").toString() == "yes"; + curStorage = tags.value("storage").toString() == "yes"; + curNick = DbManager::colText(st, 1); } + int finish = sqlite3_finalize(st); loaded = rc == SQLITE_ROW && finish == SQLITE_OK; + if (!loaded) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member properties: read ch=%s node=%016llx rc=%d finalize=%d: %s", + qPrintable(channelId), (unsigned long long)nodeId, rc, finish, sqlite3_errmsg(m_db->m_db)); + } else { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member properties: database unavailable at open"); } auto* layout = new QVBoxLayout(this); @@ -134,6 +134,9 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, "QPushButton:disabled { color: palette(midlight); }"); connect(m_transferBtn, &QPushButton::clicked, this, &MemberPropsDialog::onTransferAdmin); layout->addWidget(m_transferBtn); + m_errorLabel = new QLabel(this); m_errorLabel->setWordWrap(true); + if (!loaded) m_errorLabel->setText(QCoreApplication::translate("ChatGui", "Could not load member details.")); + layout->addWidget(m_errorLabel); layout->addStretch(); @@ -150,6 +153,7 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, btnLayout->addWidget(cancelBtn); auto* saveBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Save"), this); + m_saveBtn = saveBtn; saveBtn->setMinimumWidth(90); saveBtn->setDefault(true); saveBtn->setStyleSheet( @@ -162,29 +166,25 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, btnLayout->addWidget(saveBtn); layout->addLayout(btnLayout); + connect(this, &QDialog::finished, this, [this] { delete m_saveContext; m_saveContext = nullptr; setSaving(false); }); +} + +void MemberPropsDialog::setSaving(bool saving) { + m_saveBtn->setEnabled(!saving); m_nickEdit->setEnabled(!saving && !m_isOwn); + for (auto* box : {m_adminCb, m_superCb, m_storageCb, m_verifiedCb, m_deletedCb}) box->setEnabled(!saving && m_haveAdminKey); + m_transferBtn->setEnabled(!saving && m_haveAdminKey && !m_isOwn); +} + +void MemberPropsDialog::finishSave(int result) { + delete m_saveContext; m_saveContext = nullptr; setSaving(false); + if (!result) { accept(); return; } + m_errorLabel->setText(QCoreApplication::translate("ChatGui", "Could not save member changes. Reopen the dialog if the member has changed.")); + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member properties: save refused ch=%s node=%016llx result=%d", + qPrintable(m_channelId), (unsigned long long)m_nodeId, result); } void MemberPropsDialog::onSave() { - if (!m_db->m_db) return; - - /* 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(); - sqlite3_stmt* ns = nullptr; - if (sqlite3_prepare_v2(m_db->m_db, nickSql.constData(), -1, &ns, nullptr) == SQLITE_OK) { - sqlite3_bind_text(ns, 1, nick.isEmpty() ? nullptr : nick.toUtf8().constData(), - -1, nick.isEmpty() ? SQLITE_STATIC : SQLITE_TRANSIENT); - sqlite3_bind_int64(ns, 2, (sqlite3_int64)m_nodeId); - sqlite3_step(ns); - sqlite3_finalize(ns); - } - - /* signed fields — через chat_member_tags_commit_trampoline */ - if (m_edPrivkey.size() != 32) { accept(); return; } - + if (m_saveContext) return; int ver = m_currentVer; QString json; json += QString("{\"ver\":\"%1\"").arg(ver); @@ -195,23 +195,15 @@ void MemberPropsDialog::onSave() { json += QString(",\"deleted\":\"%1\"").arg(m_deletedCb->isChecked() ? "yes" : "no"); json += "}"; - struct chat_member_tags_req* req = (struct chat_member_tags_req*) - u_calloc(1, sizeof(struct chat_member_tags_req)); - if (!req) { accept(); return; } - - req->inst = gui_bridge_get_inst(); - 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(); + m_errorLabel->clear(); setSaving(true); m_saveContext = new QObject(this); + if (!gui_bridge_save_member(m_db ? m_db->m_db : nullptr, m_channelId, m_nodeId, m_nickEdit->text().trimmed(), + m_haveAdminKey ? json.toUtf8() : QByteArray(), m_saveContext, [this](int result) { finishSave(result); })) finishSave(-1); } void MemberPropsDialog::onTransferAdmin() { - if (m_edPrivkey.size() != 32 || m_nodeId == (uint64_t)m_db->myNodeId()) return; + if (!m_haveAdminKey || m_isOwn || m_saveContext) return; + QPointer alive(this); QString name = m_memberName.isEmpty() ? QString("0x%1").arg(m_nodeId, 16, 16, QChar('0')) : m_memberName; QMessageBox::StandardButton answer = QMessageBox::question( this, @@ -222,16 +214,7 @@ void MemberPropsDialog::onTransferAdmin() { QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - if (answer != QMessageBox::Yes) return; - - struct chat_transfer_admin_req* req = (struct chat_transfer_admin_req*) - u_calloc(1, sizeof(struct chat_transfer_admin_req)); - if (!req) return; - - req->inst = gui_bridge_get_inst(); - QByteArray ch_id_utf8 = m_channelId.toUtf8(); - strncpy(req->ch_id, ch_id_utf8.constData(), sizeof(req->ch_id) - 1); - req->target_node_id = m_nodeId; - - gui_bridge_post_uasync_fn(chat_core_transfer_admin_trampoline, req); + if (!alive || answer != QMessageBox::Yes) return; + if (!gui_bridge_transfer_admin(m_db ? m_db->m_db : nullptr, m_channelId, m_nodeId)) + m_errorLabel->setText(QCoreApplication::translate("ChatGui", "Could not request admin key transfer.")); } diff --git a/tools/chatgui/src/memberpropsdialog.h b/tools/chatgui/src/memberpropsdialog.h index 31b00de0..c8e56cbb 100644 --- a/tools/chatgui/src/memberpropsdialog.h +++ b/tools/chatgui/src/memberpropsdialog.h @@ -1,4 +1,6 @@ -/* Диалог имени и прав участника канала; сохранение подписанных изменений и передача admin-ключа через ядро. */ +/* GUI-диалог локального ника и подписанных прав участника; оба изменения фиксируются одной транзакцией ядра. + * Accepted означает подтверждённую запись, отказ оставляет черновик. Закрытие снимает callback, не отменяя принятую запись. + * Передача admin-ключа запрашивается отдельно с подтверждением пользователя; ключи остаются в ядре. */ #pragma once #include @@ -6,35 +8,45 @@ #include #include #include +#include #include class DbManager; +class QLabel; class MemberPropsDialog : public QDialog { Q_OBJECT public: + /* db заимствован через QPointer, parent владеет диалогом; channelId/nodeId фиксируют адресата. + * memberName/flags — начальный снимок отображения, JSON/ver и локальный ник читаются из БД. */ explicit MemberPropsDialog(DbManager* db, const QString& channelId, uint64_t nodeId, const QString& memberName, uint8_t flags, QWidget* parent = nullptr); private slots: - void onSave(); - void onTransferAdmin(); + void onSave(); // Снимок формы передаётся ядру один раз до результата; private key в запрос не включается. + void onTransferAdmin(); // После Yes ставит собственный запрос ядру, не подтверждает получение ключа адресатом. private: - DbManager* m_db; - QString m_channelId; - uint64_t m_nodeId; - QByteArray m_edPrivkey; - uint8_t m_flags; - int m_currentVer; - QString m_memberName; + void setSaving(bool saving); // Блокирует редактирование/повторную передачу ключа, оставляя возможность закрыть форму. + void finishSave(int result); // Успех закрывает форму; ошибка восстанавливает ввод и показывает отказ. + QPointer m_db; // Заимствованная read-only проекция; не передаётся в worker. + QString m_channelId; // Собственный ID канала. + uint64_t m_nodeId; // Адресат сохранения/передачи ключа. + bool m_haveAdminKey = false; // На момент открытия БД содержала ключ; ядро проверяет полномочия заново. + bool m_isOwn = false; // Собственный ник не редактируется, передача ключа себе запрещена. + uint8_t m_flags; // Исходные флаги для admin/super checkboxes. + int m_currentVer; // Версия owner-блока при открытии: предотвращает перезапись устаревшей формой. + QString m_memberName; // Имя в предупреждении о передаче ключа. - QLineEdit* m_nickEdit; - QCheckBox* m_adminCb; - QCheckBox* m_superCb; - QCheckBox* m_storageCb; - QCheckBox* m_verifiedCb; - QCheckBox* m_deletedCb; - QPushButton* m_transferBtn; + QLineEdit* m_nickEdit; // Локальный неподписанный псевдоним, пустой снимает его. + QCheckBox* m_adminCb; // Желаемая роль admin; историю и подпись строит ядро. + QCheckBox* m_superCb; // supernode=yes/no. + QCheckBox* m_storageCb; // storage=yes/no. + QCheckBox* m_verifiedCb; // verified=yes/no. + QCheckBox* m_deletedCb; // deleted=yes/no. + QPushButton* m_transferBtn; // Доступна с admin-ключом для другого участника. + QPushButton* m_saveBtn; // Отключена во время записи. + QLabel* m_errorLabel; // Ошибка без технических подробностей; причина остаётся в логе. + QObject* m_saveContext = nullptr; // Собственный context запроса; уничтожение снимает callback в мосте. };