/* * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. * * SPDX-License-Identifier: BSD-3-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class AppTranslator : public QTranslator { Q_OBJECT public: explicit AppTranslator(QObject *parent) : QTranslator(parent) { qApp->installTranslator(this); } ~AppTranslator() override { qApp->removeTranslator(this); } }; class FediverseInstanceIconCache : public QObject { Q_OBJECT public: explicit FediverseInstanceIconCache(QObject *parent = nullptr) : QObject(parent) , m_networkAccessManager(new QNetworkAccessManager(this)) { } Q_INVOKABLE QString cachedIconPath(const QString &apiHost) const { const QString normalizedHost = normalizeApiHost(apiHost); if (normalizedHost.isEmpty()) { return QString(); } const QString prefix = iconPrefix(normalizedHost); const QDir dir(cacheDirectory()); const QStringList matches = dir.entryList(QStringList() << (prefix + QStringLiteral(".*")), QDir::Files); if (matches.isEmpty()) { return QString(); } return dir.absoluteFilePath(matches.first()); } Q_INVOKABLE void cacheIcon(const QString &apiHost, const QString &iconUrl) { const QString normalizedHost = normalizeApiHost(apiHost); const QUrl normalizedUrl = QUrl::fromUserInput(iconUrl); if (normalizedHost.isEmpty() || !normalizedUrl.isValid() || normalizedUrl.scheme().isEmpty()) { emit iconError(apiHost); return; } const QString existingPath = cachedIconPath(normalizedHost); if (!existingPath.isEmpty()) { emit iconReady(normalizedHost, existingPath); return; } if (m_hostsInFlight.contains(normalizedHost)) { return; } QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(normalizedUrl)); reply->setProperty("apiHost", normalizedHost); connect(reply, &QNetworkReply::finished, this, [this, reply]() { handleReply(reply); }); m_hostsInFlight.insert(normalizedHost); } Q_SIGNALS: void iconReady(const QString &apiHost, const QString &iconPath); void iconError(const QString &apiHost); private: static QString normalizeApiHost(const QString &rawHost) { QString host = rawHost.trimmed(); if (host.isEmpty()) { return QString(); } if (!host.startsWith(QLatin1String("https://")) && !host.startsWith(QLatin1String("http://"))) { host.prepend(QStringLiteral("https://")); } const QUrl url(host); if (!url.isValid() || url.host().isEmpty()) { return QString(); } QString normalized = QString::fromLatin1(url.toEncoded(QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment)); if (normalized.endsWith(QLatin1Char('/'))) { normalized.chop(1); } return normalized; } static QString cacheDirectory() { const QString base = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation); return base + QStringLiteral("/sailfish-account-fediverse/icons"); } static QString iconPrefix(const QString &apiHost) { return QString::fromLatin1(QCryptographicHash::hash(apiHost.toUtf8(), QCryptographicHash::Sha1).toHex()); } static QString extensionForReply(QNetworkReply *reply) { const QString suffix = QFileInfo(reply->url().path()).suffix().trimmed().toLower(); if (!suffix.isEmpty()) { return suffix; } const QString contentType = QString::fromLatin1(reply->header(QNetworkRequest::ContentTypeHeader).toByteArray()).toLower(); if (contentType.contains(QLatin1String("png"))) { return QStringLiteral("png"); } if (contentType.contains(QLatin1String("jpeg")) || contentType.contains(QLatin1String("jpg"))) { return QStringLiteral("jpg"); } if (contentType.contains(QLatin1String("webp"))) { return QStringLiteral("webp"); } if (contentType.contains(QLatin1String("svg"))) { return QStringLiteral("svg"); } return QStringLiteral("img"); } void handleReply(QNetworkReply *reply) { const QString apiHost = reply->property("apiHost").toString(); m_hostsInFlight.remove(apiHost); const int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const bool success = reply->error() == QNetworkReply::NoError && httpCode >= 200 && httpCode < 300; const QString extension = extensionForReply(reply); const QByteArray data = reply->readAll(); reply->deleteLater(); if (!success || data.isEmpty()) { emit iconError(apiHost); return; } const QString dirPath = cacheDirectory(); QDir dir; if (!dir.mkpath(dirPath)) { emit iconError(apiHost); return; } const QString prefix = iconPrefix(apiHost); const QDir cacheDir(dirPath); const QStringList matches = cacheDir.entryList(QStringList() << (prefix + QStringLiteral(".*")), QDir::Files); for (const QString &match : matches) { QFile::remove(cacheDir.absoluteFilePath(match)); } const QString filePath = cacheDir.absoluteFilePath(prefix + QStringLiteral(".") + extension); QSaveFile file(filePath); if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) { emit iconError(apiHost); return; } emit iconReady(apiHost, filePath); } QNetworkAccessManager *m_networkAccessManager; QSet m_hostsInFlight; }; class FediverseAccountsTranslationsPlugin : public QQmlExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID "com.jolla.settings.accounts.fediverse") public: void initializeEngine(QQmlEngine *engine, const char *uri) override { Q_UNUSED(uri) AppTranslator *engineeringEnglish = new AppTranslator(engine); engineeringEnglish->load("settings-accounts-fediverse_eng_en", "/usr/share/translations"); AppTranslator *translator = new AppTranslator(engine); translator->load(QLocale(), "settings-accounts-fediverse", "-", "/usr/share/translations"); } void registerTypes(const char *uri) override { Q_ASSERT(QLatin1String(uri) == QLatin1String("com.jolla.settings.accounts.fediverse")); qmlRegisterUncreatableType(uri, 1, 0, "FediverseTranslationPlugin", QString()); qmlRegisterType(uri, 1, 0, "FediverseInstanceIconCache"); } }; #include "plugin.moc"