/* * This file is part of the Flowee project * Copyright (C) 2023-2025 Tom Zander * * 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 "ModuleManager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include enum ModuleConfigSaveTags { ModuleId = 1, LegacyModuleSectionId, // Deprecated (since 2024-10) LegacyModuleSectionEnabled, // Deprecated (since 2024-10) ModuleEnabled, // bool ModuleSaveData // byte-array for a module to save local config. }; static ModuleManager::FrontEnd g_moduleManager_FrontEnd = ModuleManager::Desktop; ModuleManager::ModuleManager(QObject *parent) : QObject(parent) { m_configFile = QStandardPaths::locate(QStandardPaths::AppConfigLocation, "modules.conf"); if (m_configFile.isEmpty()) { // make sure the directory exists auto path = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); std::filesystem::create_directories(path.toStdString()); m_configFile = path + '/' + "modules.conf"; } #ifdef TARGET_OS_Android auto guiApp = qobject_cast(QCoreApplication::instance()); assert(guiApp); connect(guiApp, &QGuiApplication::applicationStateChanged, this, [=](Qt::ApplicationState state) { if (state == Qt::ApplicationInactive || state == Qt::ApplicationSuspended) save(); }); #endif extern void load_all_modules(ModuleManager*); // the load_all_modules method is generated by cmake in the modules builddir. // it essentially makes each module call 'load' on this class. load_all_modules(this); // connect to signals of the sections so we tell the QML to get the new // filtered list on the user enabling / disabling something. for (const auto *m : std::as_const(m_modules)) { for (auto *s : m->sections()) { connect (s, &ModuleSection::enabledChanged, this, [=]() { switch (s->type()) { case ModuleSection::SendMethod: emit sendMenuSectionsChanged(); case ModuleSection::MainMenuItem: emit mainMenuSectionsChanged(); case ModuleSection::OtherSectionType: emit exploreTabItemsChanged(); default: break; } }); } } } ModuleManager::~ModuleManager() { save(); } void ModuleManager::setFrontEnd(FrontEnd fe) { g_moduleManager_FrontEnd = fe; } ModuleManager::FrontEnd ModuleManager::frontEnd() { return g_moduleManager_FrontEnd; } void ModuleManager::load(const char *translationUnit, const std::function &function) { if (translationUnit) { QString translations = QString::fromUtf8(translationUnit); auto *translator = new QTranslator(this); /* * We try to load it from the default location as used in the translations subdir (:/i18n) * and if that fails we try again at the root level of the QRC because individual modules * can then just use one data.qrc and include translations, if they manage their own. */ if (translator->load(QLocale(), translations, QLatin1String("_"), QLatin1String(":/i18n"))) QCoreApplication::installTranslator(translator); else if (translator->load(QLocale(), translations, QLatin1String("_"), QLatin1String(":"))) QCoreApplication::installTranslator(translator); else delete translator; } auto *info = function(); if (info == nullptr) return; if (info->enabled()) { logCritical() << "ModuleInfo starts 'enabled', denying user choice. Cowerdly refusing to register it"; return; } info->setParent(this); // take ownership int insertPoint = m_modules.size(); while (insertPoint > 0) { if (m_modules.at(insertPoint - 1)->priority() >= info->priority()) break; --insertPoint; } m_modules.insert(insertPoint, info); connect (info, &ModuleInfo::requestSaveSettings, this, [=]() { QTimer::singleShot(2000, this, &ModuleManager::save); }); } void ModuleManager::loadConfiguration() { std::ifstream in(m_configFile.toStdString()); if (!in.is_open()) return; auto dataSize = boost::filesystem::file_size(m_configFile.toStdString()); Streaming::BufferPool pool(dataSize); in.read(pool.data(), dataSize); Streaming::MessageParser parser(pool.commit(dataSize)); ModuleInfo *info = nullptr; int type = -1; bool prevEnabledCalled = false; // make sure we call those regardless of what the save-file contains bool prevSettingsLoaded = false; while (parser.next() == Streaming::FoundTag) { if (parser.tag() == ModuleId) { if (info && !prevEnabledCalled) info->setEnabled(false); if (info && !prevSettingsLoaded) info->loadSettings(Streaming::ConstBuffer()); info = nullptr; prevEnabledCalled = false; prevSettingsLoaded = false; type = -1; const QString wantedId = QString::fromUtf8(parser.stringData()); for (auto *module : std::as_const(m_modules)) { if (module->id() == wantedId) { info = module; break; } } } // legacy support else if (parser.tag() == LegacyModuleSectionId) { type = parser.intData(); } // legacy support else if (parser.tag() == LegacyModuleSectionEnabled) { if (info) { info->setEnabled(info->enabled() || parser.boolData()); for (auto *s : info->sections()) { if (s->type() == type) { s->setEnabled(parser.boolData()); break; } } } } else if (parser.tag() == ModuleEnabled) { if (info) info->setEnabled(parser.boolData()); prevEnabledCalled = true; } else if (parser.tag() == ModuleSaveData) { if (info) info->loadSettings(parser.bytesDataBuffer()); prevSettingsLoaded = true; } } } void ModuleManager::save() const { if (m_modules.isEmpty()) { // be nice to the devs that use both mobile and non-mobile on their // account, the non-mobile has no modules, as such we just don't // (over)write the config file. return; } int saveFileSize = 100; auto pool = std::make_shared(100000); QList saveData; for (const auto *m : m_modules) { saveFileSize += m->id().size() * 3 + 3; saveData.append(m->saveSettings(pool)); saveFileSize += saveData.back().size() + 12; } pool->reserve(saveFileSize); Streaming::MessageBuilder builder(pool); for (int i = 0; i < m_modules.size(); ++i) { const auto *m = m_modules.at(i); builder.add(ModuleId, m->id().toStdString()); if (m->enabled()) builder.add(ModuleEnabled, true); auto saveFile = saveData.at(i); if (!saveFile.isEmpty()) builder.add(ModuleSaveData, saveFile); } auto data = builder.buffer(); // hash the new file and check if its different lest we can skip saving QFile origFile(m_configFile); if (origFile.open(QIODevice::ReadOnly)) { CRIPEMD160 fileHasher; auto origContent = origFile.readAll(); fileHasher.write(origContent.data(), origContent.size()); char fileHash[CRIPEMD160::OUTPUT_SIZE]; fileHasher.finalize(fileHash); CRIPEMD160 memHasher; memHasher.write(data.begin(), data.size()); char memHash[CRIPEMD160::OUTPUT_SIZE]; memHasher.finalize(memHash); if (memcmp(fileHash, memHash, CRIPEMD160::OUTPUT_SIZE) == 0) { // no changes, so don't write. return; } } try { auto configFile = m_configFile.toStdString(); auto configFileNew = configFile + "~"; std::ofstream outFile(configFileNew); outFile.write(data.begin(), data.size()); outFile.flush(); outFile.close(); std::filesystem::rename(configFileNew, configFile); } catch (const std::exception &e) { logFatal() << "Failed to save the wallet.dat. Reason:" << e; } } QList ModuleManager::registeredModules() const { return m_modules; } bool ModuleManager::isModuleAvailable(const QString &id) const { for (const auto *m : m_modules) { if (m->id() == id) return true; } return false; } QList ModuleManager::sendMenuSections() const { QList answer; for (const auto *m : m_modules) { for (auto *s : m->sections()) { if (s->enabled() && s->type() == ModuleSection::SendMethod) answer.append(s); } } return answer; } QList ModuleManager::mainMenuSections() const { QList answer; for (const auto *m : m_modules) { for (auto *s : m->sections()) { if (s->enabled() && s->type() == ModuleSection::MainMenuItem) answer.append(s); } } return answer; } QList ModuleManager::exploreTabItems() const { QList answer; for (const auto *m : m_modules) { for (auto *s : m->sections()) { if (s->type() == ModuleSection::OtherSectionTypeDefault || (s->enabled() && s->type() == ModuleSection::OtherSectionType)) answer.append(s); } } return answer; } QList ModuleManager::oftenUsedItems() const { QList answer; // TODO return answer; } ModuleSection *ModuleManager::sectionOnPlugin(const QString &pluginId, const QString §ionId) const { for (const auto *m : m_modules) { if (m->id() == pluginId) { for (auto *s : m->sections()) { if (s->type() == ModuleSection::CustomSectionType && s->sectionId() == sectionId) return s; } break; } } return nullptr; } ModuleInfo *ModuleManager::moduleInfo(const QString &pluginId) const { for (auto *m : m_modules) { if (m->id() == pluginId) { return m; } } return nullptr; } ModuleSection *ModuleManager::moduleSection(const QString &pluginId) const { for (const auto *m : m_modules) { if (m->id() == pluginId) { if (m->sections().empty()) return nullptr; for (auto *s : m->sections()) { if (s->type() == ModuleSection::OtherSectionType) return s; } return m->sections().first(); } } return nullptr; } void ModuleManager::allSections(const std::function &handler) { for (const auto *m : std::as_const(m_modules)) { for (auto *s : m->sections()) { handler(m->id(), s); } } } void ModuleManager::requestAddPage(const QString &url, QObject *dataObject) { emit addPage(url, dataObject); } void ModuleManager::markChild(QObject *parent, QObject *child) { if (parent && child) child->setParent(parent); }