/* SQLite-представление данных для desktop UI: каналы/сообщения/узлы, локальные имена и диагностика БД. * setDb заимствует handle ядра; каждый запрос закрывает свой statement до возврата, SQLite остаётся ядру. * Перед уничтожением БД сбросить ссылку. Сетевые и подписанные изменения идут через chat_core; * методы чтения возвращают самостоятельные Qt-значения. */ #ifndef DB_MANAGER_H #define DB_MANAGER_H #include #include #include #include #include #include #include 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 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 getMessages(const QString& chId, int limit = 50, const QByteArray& around = {}) const; QList 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 getDmConversations() const; /* ── Участники канала (UI) ── */ QString getDisplayName(quint64 nodeId) const; QMap getLocalNicknames(const QString& chId) const; bool isChannelMember(const QString& chId, quint64 nodeId) const; /* ── Узлы ── */ QByteArray getNodeEdPub(quint64 nodeId) const; /* ── Аккаунты ── */ QList 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