169 lines
11 KiB
C++
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
|