/* * Squawk messenger. * Copyright (C) 2019 Yury Gubich * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "archive.h" #include #include #include Core::Archive::Archive(const QString& account, const QString& p_jid, QObject* parent): QObject(parent), jid(p_jid), account(account), opened(false), db(account + "/" + jid), messages(db.addStorage("messages")), order(db.addStorage("order", true)), stats(db.addStorage("stats")), avatars(db.addStorage("avatars")), stanzaIdToId(db.addStorage("stanzaIdToId")), cursor(order->createCursor()) {} Core::Archive::~Archive() { close(); } void Core::Archive::open() { db.open(); LMDBAL::WriteTransaction txn = db.beginTransaction(); AvatarInfo info; bool hasAvatar = false; try { avatars->getRecord(jid, info, txn); hasAvatar = true; } catch (const LMDBAL::NotFound& e) {} if (!hasAvatar) return; QFile ava(db.getPath() + "/" + jid + "." + info.type); if (ava.exists()) return; try { avatars->removeRecord(jid, txn); txn.commit(); } catch (const std::exception& e) { qDebug() << e.what(); qDebug() << "error opening archive" << jid << "for account" << account << ". There is supposed to be avatar but the file doesn't exist, couldn't even drop it, it surely will lead to an error"; } } void Core::Archive::close() { db.close(); } bool Core::Archive::addElement(const Shared::Message& message) { QString id = message.getId(); qDebug() << "Adding message with id " << id; try { LMDBAL::WriteTransaction txn = db.beginTransaction(); messages->addRecord(id, message, txn); order->addRecord(message.getTime().toMSecsSinceEpoch(), id, txn); QString stanzaId = message.getStanzaId(); if (!stanzaId.isEmpty()) stanzaIdToId->addRecord(stanzaId, id, txn); txn.commit(); return true; } catch (const std::exception& e) { qDebug() << "Could not add message with id " + id; qDebug() << e.what(); } return false; } void Core::Archive::clear() { db.drop(); } Shared::Message Core::Archive::getElement(const QString& id) const { return messages->getRecord(id); } bool Core::Archive::hasElement(const QString& id) const { return messages->checkRecord(id); } void Core::Archive::changeMessage(const QString& id, const QMap& data) { LMDBAL::WriteTransaction txn = db.beginTransaction(); Shared::Message msg = messages->getRecord(id, txn); bool hadStanzaId = !msg.getStanzaId().isEmpty(); QDateTime oTime = msg.getTime(); bool idChange = msg.change(data); QString newId = msg.getId(); QDateTime nTime = msg.getTime(); bool orderChange = oTime != nTime; if (idChange || orderChange) { if (idChange) messages->removeRecord(id, txn); if (orderChange) order->removeRecord(oTime.toMSecsSinceEpoch(), txn); order->forceRecord(nTime.toMSecsSinceEpoch(), newId, txn); } QString sid = msg.getStanzaId(); if (!sid.isEmpty() && (idChange || !hadStanzaId)) stanzaIdToId->forceRecord(sid, newId, txn); messages->forceRecord(newId, msg, txn); txn.commit(); } Shared::Message Core::Archive::newest() const { LMDBAL::Transaction txn = db.beginReadOnlyTransaction(); try { cursor.open(txn); while (true) { std::pair pair = cursor.prev(); Shared::Message msg = messages->getRecord(pair.second, txn); if (msg.serverStored()) { cursor.close(); return msg; } } } catch (...) { cursor.close(); throw; } } QString Core::Archive::newestId() const { Shared::Message msg = newest(); return msg.getId(); } QString Core::Archive::oldestId() const { Shared::Message msg = oldest(); return msg.getId(); } Shared::Message Core::Archive::oldest() const { LMDBAL::Transaction txn = db.beginReadOnlyTransaction(); try { cursor.open(txn); while (true) { std::pair pair = cursor.next(); Shared::Message msg = messages->getRecord(pair.second, txn); if (msg.serverStored()) { cursor.close(); return msg; } } } catch (...) { cursor.close(); throw; } } unsigned int Core::Archive::addElements(const std::list& messages) { unsigned int success = 0; LMDBAL::WriteTransaction txn = db.beginTransaction(); for (const Shared::Message& message : messages) { QString id = message.getId(); bool added = false; try { Core::Archive::messages->addRecord(id, message, txn); added = true; } catch (const LMDBAL::Exist& e) {} if (!added) continue; order->addRecord(message.getTime().toMSecsSinceEpoch(), id, txn); QString sid = message.getStanzaId(); if (!sid.isEmpty()) stanzaIdToId->addRecord(sid, id, txn); ++success; } txn.commit(); return success; } long unsigned int Core::Archive::size() const { return order->count(); } std::list Core::Archive::getBefore(unsigned int count, const QString& id) { LMDBAL::Transaction txn = db.beginReadOnlyTransaction(); std::list res; try { cursor.open(txn); if (!id.isEmpty()) { Shared::Message reference = messages->getRecord(id, txn); uint64_t stamp = reference.getTime().toMSecsSinceEpoch(); cursor.set(stamp); } for (unsigned int i = 0; i < count; ++i) { std::pair pair; cursor.prev(pair.first, pair.second); res.emplace_back(); Shared::Message& msg = res.back(); messages->getRecord(pair.second, msg, txn); } cursor.close(); return res; } catch (const LMDBAL::NotFound& e) { cursor.close(); if (res.empty()) throw e; else return res; } catch (...) { cursor.close(); throw; } } bool Core::Archive::isFromTheBeginning() const { try { return stats->getRecord("fromTheBeginning").toBool(); } catch (const LMDBAL::NotFound& e) { return false; } } void Core::Archive::setFromTheBeginning(bool is) { stats->forceRecord("fromTheBeginning", is); } Shared::EncryptionProtocol Core::Archive::encryption() const { try { return stats->getRecord("encryption").value(); } catch (const LMDBAL::NotFound& e) { return Shared::EncryptionProtocol::none; } } bool Core::Archive::setEncryption(Shared::EncryptionProtocol is) { LMDBAL::WriteTransaction txn = db.beginTransaction(); Shared::EncryptionProtocol current = Shared::EncryptionProtocol::none; try { current = stats->getRecord("encryption", txn).value(); } catch (const LMDBAL::NotFound& e) {} if (is != current) { stats->forceRecord("encryption", static_cast(is), txn); txn.commit(); return true; } return false; } QString Core::Archive::idByStanzaId(const QString& stanzaId) const { return stanzaIdToId->getRecord(stanzaId); } QString Core::Archive::stanzaIdById(const QString& id) const { try { Shared::Message msg = getElement(id); return msg.getStanzaId(); } catch (const LMDBAL::NotFound& e) { return QString(); } } bool Core::Archive::setAvatar(const QByteArray& data, AvatarInfo& newInfo, bool generated, const QString& resource) { LMDBAL::WriteTransaction txn = db.beginTransaction(); AvatarInfo oldInfo; bool haveAvatar = false; QString res = resource.isEmpty() ? jid : resource; try { avatars->getRecord(res, oldInfo, txn); haveAvatar = true; } catch (const LMDBAL::NotFound& e) {} if (data.size() == 0) { if (!haveAvatar) return false; avatars->removeRecord(res, txn); txn.commit(); return true; } QString currentPath = db.getPath(); bool needToRemoveOld = false; QCryptographicHash hash(QCryptographicHash::Sha1); hash.addData(data); QByteArray newHash(hash.result()); if (haveAvatar) { if (!generated && !oldInfo.autogenerated && oldInfo.hash == newHash) return false; QFile oldAvatar(currentPath + "/" + res + "." + oldInfo.type); if (oldAvatar.exists()) { if (oldAvatar.rename(currentPath + "/" + res + "." + oldInfo.type + ".bak")) { needToRemoveOld = true; } else { qDebug() << "Can't change avatar: couldn't get rid of the old avatar" << oldAvatar.fileName(); return false; } } } QMimeDatabase mimedb; QMimeType type = mimedb.mimeTypeForData(data); QString ext = type.preferredSuffix(); QFile newAvatar(currentPath + "/" + res + "." + ext); if (!newAvatar.open(QFile::WriteOnly)) { qDebug() << "Can't change avatar: cant open file to write" << newAvatar.fileName() << "rolling back to the previous state"; if (needToRemoveOld) { QFile oldAvatar(currentPath + "/" + res + "." + oldInfo.type + ".bak"); oldAvatar.rename(currentPath + "/" + res + "." + oldInfo.type); } return false; } newAvatar.write(data); newAvatar.close(); newInfo.type = ext; newInfo.hash = newHash; newInfo.autogenerated = generated; try { avatars->forceRecord(res, newInfo, txn); txn.commit(); } catch (...) { qDebug() << "Can't change avatar: couldn't store changes to database for" << newAvatar.fileName() << "rolling back to the previous state"; if (needToRemoveOld) { QFile oldAvatar(currentPath + "/" + res + "." + oldInfo.type + ".bak"); oldAvatar.rename(currentPath + "/" + res + "." + oldInfo.type); } return false; } if (needToRemoveOld) { QFile oldAvatar(currentPath + "/" + res + "." + oldInfo.type + ".bak"); oldAvatar.remove(); } return true; } bool Core::Archive::readAvatarInfo(Core::Archive::AvatarInfo& target, const QString& resource) const { try { avatars->getRecord(resource.isEmpty() ? jid : resource, target); return true; } catch (const LMDBAL::NotFound& e) { return false; } } void Core::Archive::readAllResourcesAvatars(std::map& data) const { avatars->readAll(data); } Core::Archive::AvatarInfo Core::Archive::getAvatarInfo(const QString& resource) const { return avatars->getRecord(resource); } Core::Archive::AvatarInfo::AvatarInfo(): type(), hash(), autogenerated(false) {} Core::Archive::AvatarInfo::AvatarInfo(const QString& p_type, const QByteArray& p_hash, bool p_autogenerated): type(p_type), hash(p_hash), autogenerated(p_autogenerated) {} QDataStream & operator<<(QDataStream& out, const Core::Archive::AvatarInfo& info) { out << info.type; out << info.hash; out << info.autogenerated; return out; } QDataStream & operator>>(QDataStream& in, Core::Archive::AvatarInfo& info) { in >> info.type; in >> info.hash; in >> info.autogenerated; return in; }