Files
utun2/tools/chatgui/db/db_manager.h
T

169 lines
11 KiB
C++

/* SQLite-представление данных для desktop UI: каналы/сообщения/узлы, локальные имена и диагностика БД.
* setDb заимствует handle ядра; каждый запрос закрывает свой statement до возврата, SQLite остаётся ядру.
* Перед уничтожением БД сбросить ссылку. Сетевые и подписанные изменения идут через chat_core;
* методы чтения возвращают самостоятельные Qt-значения. */
#ifndef DB_MANAGER_H
#define DB_MANAGER_H
#include <sqlite3.h>
#include <QString>
#include <QStringList>
#include <QList>
#include <QMap>
#include <QObject>
#include <cstdint>
enum class ChannelSortMode { Alphabetical, Recent }; // По имени либо last_msg_at DESC с именем для равенства.
/* Поля channels и переопределённые ui_state позиции; ключи/signature — самостоятельные BLOB-копии. */
struct ChannelRow {
QString channelId;
QString name;
quint64 ownerNodeId = 0;
QByteArray x25519_pubkey;
QByteArray x25519_privkey;
QByteArray ed25519_pubkey;
QByteArray ed25519_privkey;
QByteArray signature; // Ed25519 подпись всех полей записи privkey канала
qint64 lastReadMsgId = 0;
qint64 lastPosMsgId = 0;
qint64 lastMsgAt = 0;
qint64 createdAt = 0;
int unreadCount = 0;
quint64 avatarVersion = 0;
QByteArray avatarHash;
};
struct MessageRow {
qint64 id = 0; // Локальный rowid, 0 означает отсутствие/ошибку lookup.
quint64 authorNodeId = 0;
QString contentType;
QByteArray data; // Декодированная строка d, UTF-8; не полный JSON из SQLite.
qint64 timestamp = 0;
quint64 version = 1;
qint64 editedAt = 0;
QByteArray signature;
QByteArray messageId; // общий ID подписанного сообщения, 32 байта
QByteArray replyTo; // общий ID оригинала в этом канале, либо пусто
QByteArray localAttrs; // Локальный JSON загрузки/пути, вне подписанного тела.
bool isOutgoing = false; // Автор совпал с локальной идентичностью.
};
/* Самостоятельный снимок расшифровки вложения, без SQLite-указателей. */
struct AttachmentRow {
bool found = false; // Строка получена целиком без SQL-ошибок.
bool ready = false, downloaded = false, decrypting = false, error = false; // Состояния подготовки plaintext.
bool hasKey = false; // В БД имеется 32-байтный ключ.
int requestState = -1; // Состояние исходящего запроса ключа; -1 — запроса нет.
QString path, contentType; // Plaintext-файл и его MIME; существование файла проверяет UI.
QByteArray info; // Кодированный attachment_info, валидируется attachment_decode.
};
struct AccountRow {
quint64 nodeId = 0;
QString displayName;
QString avatarColor;
QString avatarLetter;
bool isContact = true;
};
/* Результат синхронной проверки/обслуживания SQLite; успех optimize не подтверждает целостность данных. */
struct IntegrityResult {
bool ok = false; // Проверка: SQL успешен и получен ok без нарушений; optimize: SQL завершён успешно.
QString message; // ok при успехе, иначе первая диагностика проверки либо ошибка SQL/отсутствия БД.
QStringList errors; // Дополнительные сообщения integrity-проверки после первого.
int elapsedMs = 0; // Время SQL-вызова, мс; 0 также при отсутствии handle.
};
struct DmConversation {
QString convId;
quint64 generation = 0; /* Поколение видимой истории: фиксируется в действиях PM до передачи ядру. */
quint64 peerNodeId = 0;
QString peerName;
qint64 lastTs = 0; // Время последнего сообщения для сортировки UI.
qint64 lastOutSeq = 0; // Последний исходящий seq.
qint64 lastInSeq = 0; // Последний входящий seq; пространства направлений независимы.
};
class DbManager : public QObject {
Q_OBJECT
public:
/* GUI-представление: parent владеет QObject, внешний владелец — SQLite. Read API не возвращает statements.
* Ошибка SQL логируется и даёт пустой снимок/значение по умолчанию, без частичных строк.
* Для ядра UtunNode снять handle в stopping до его подтверждения; m_dbPath — каталог media, не имя chats.db. */
explicit DbManager(QObject* parent = nullptr);
~DbManager() override;
bool isOpen() const { return m_db != nullptr; }
void setDb(sqlite3* db) { m_db = db; }
void setDbPath(const QString& path) { m_dbPath = path.isEmpty() ? QStringLiteral(".") : path; }
QString dbPath() const { return m_dbPath; }
quint64 myNodeId() const { return m_myNodeId; }
void setMyNodeId(quint64 nodeId) { m_myNodeId = nodeId; }
/* ── Каналы ── */
QList<ChannelRow> getChannels(ChannelSortMode sort = ChannelSortMode::Alphabetical) const; // ui_state переопределяет read/scroll.
/* Читает BLOB аватара канала <=16 KiB. Возвращает самостоятельные байты; при ошибке/нет строки — пустой массив.
* Необязательный ok отличает пустой BLOB (удаление) или NULL при version=0 (ещё не публиковался) от отказа.
* bind/step/finalize проверяются, отказ не выдаёт успешные/частичные байты.
* Хеш/версию и формат JPEG проверяет GroupAvatarManager, запись не меняется. */
QByteArray getChannelAvatar(const QString& channelId, bool* ok = nullptr) const;
QString getChannelLastMessage(const QString& channelId) const;
int getUnreadCount(const QString& channelId, qint64 lastReadMsgId) const;
/* ── UI state for channel read/scroll ── */
qint64 getUiStateInt64(const QString& key, qint64 defaultVal = 0) const;
/* ── Сообщения (UI) ── */
/* Последние limit>0 сообщений по timestamp/message_id, результат от старых к новым.
* around: пусто либо 32-байтный ID окна вокруг оригинала. Since использует локальный rowid, не timestamp. */
QList<MessageRow> getMessages(const QString& chId, int limit = 50, const QByteArray& around = {}) const;
QList<MessageRow> getMessagesSince(const QString& chId, qint64 sinceId) const;
MessageRow getMessageById(const QString& chId, qint64 msgId) const;
MessageRow getMessageByKey(const QString& chId, const QByteArray& key) const;
AttachmentRow getAttachment(const QString& channel, const QByteArray& messageId) const; // Ключ ровно 32 байта; ошибка → found=false.
static QString messagePreview(const MessageRow& message); // Однострочный текст/описание медиа, до 160 символов.
/* ── DM-беседы (UI, read-only) ── */
QList<DmConversation> getDmConversations() const;
/* ── Участники канала (UI) ── */
QString getDisplayName(quint64 nodeId) const;
QMap<quint64, QString> getLocalNicknames(const QString& chId) const;
bool isChannelMember(const QString& chId, quint64 nodeId) const;
/* ── Узлы ── */
QByteArray getNodeEdPub(quint64 nodeId) const;
/* ── Аккаунты ── */
QList<AccountRow> getAccounts(bool contactsOnly = false) const;
AccountRow getAccount(quint64 nodeId) const;
/* ── Чтение UI state из SQLite ── */
QString getUiState(const QString& key) const;
int getUiStateInt(const QString& key, int defaultVal) const;
/* Низкоуровневое read-only API: caller проверяет bind/step и всегда вызывает sqlite3_finalize.
* Не хранить statement между GUI-событиями/остановкой ядра; методы выше предпочтительнее. */
sqlite3_stmt* prepareOrNull(const char* sql) const;
static QString colText(sqlite3_stmt* stmt, int col);
static QByteArray colBlob(sqlite3_stmt* stmt, int col);
QString msgTableName(const QString& chId) const;
QString peersTableName(const QString& chId) const;
static QString sanitizeTableName(const QString& chId);
/* Синхронные PRAGMA на заимствованном handle; вызывающий сохраняет его живым до возврата, ошибки логируются в CONFIG. */
IntegrityResult integrityCheckQuick() const; // quick_check: требует ответ ok, нарушения в строках SQLite означают отказ.
IntegrityResult integrityCheckFull() const; // integrity_check: тот же контракт, полная проверка со сверкой индексов.
IntegrityResult integrityOptimize() const; // optimize: SQL-успех допустим без строк результата; не является integrity-проверкой.
sqlite3* m_db = nullptr; // Заимствован, сбрасывается в GUI до destroy владельца. Не писать из GUI через этот handle.
QString m_dbPath = QStringLiteral("."); // Фактический каталог профиля; пустой native db_path означает cwd.
quint64 m_myNodeId = 0; // Копия node_id для сравнения авторов; 0 — ещё не опубликована.
};
#endif