// SPDX-FileCopyrightText: 2021 kaniini // SPDX-FileCopyrightText: 2021 Carl Schwan // SPDX-License-Identifier: GPL-3.0-only #include "account/accountmanager.h" #include "account/account.h" #include "config.h" #include "network/networkaccessmanagerfactory.h" #include "tokodon_debug.h" #include using namespace Qt::Literals::StringLiterals; AccountManager::AccountManager(QObject *parent) : QAbstractListModel(parent) , m_selected_account(nullptr) , m_qnam(NetworkAccessManagerFactory().create(this)) , m_notificationHandler(new NotificationHandler(m_qnam, this)) { } AccountManager::~AccountManager() { for (auto a : std::as_const(m_accounts)) { delete a; } m_accounts.clear(); } QVariant AccountManager::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return {}; } auto account = m_accounts.at(index.row()); if (account->identity() == nullptr) { return {}; } switch (role) { case Qt::DisplayRole: case DisplayNameRole: if (!account->identity()->displayNameHtml().isEmpty()) return account->identity()->displayNameHtml(); else return account->username(); case DescriptionRole: return account->identity()->account(); case InstanceRole: return account->instanceName(); case AccountRole: return QVariant::fromValue(m_accounts[index.row()]); } return {}; } int AccountManager::rowCount(const QModelIndex &index) const { Q_UNUSED(index) return m_accounts.size(); } QHash AccountManager::roleNames() const { return { {Qt::DisplayRole, QByteArrayLiteral("display")}, {DisplayNameRole, QByteArrayLiteral("displayName")}, {AccountRole, QByteArrayLiteral("account")}, {DescriptionRole, QByteArrayLiteral("description")}, {InstanceRole, QByteArrayLiteral("instance")}, }; } AccountManager &AccountManager::instance() { static AccountManager accountManager; return accountManager; } AbstractAccount *AccountManager::createNewAccount(const QString &instanceUri, bool ignoreSslErrors, bool admin) { return new Account(instanceUri, m_qnam, ignoreSslErrors, admin, this); } bool AccountManager::hasAccounts() const { return !m_accounts.empty(); } bool AccountManager::hasAnyAccounts() const { return m_hasAnyAccounts; } NotificationHandler *AccountManager::notificationHandler() const { return m_notificationHandler; } void AccountManager::addAccount(AbstractAccount *account, bool skipAuthenticationCheck) { beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size()); m_accounts.append(account); int acctIndex = m_accountStatus.size(); if (!skipAuthenticationCheck) { if (m_testMode) { m_accountStatus.push_back(AccountStatus::Loaded); } else { m_accountStatus.push_back(AccountStatus::NotLoaded); } m_accountStatusStrings.push_back({}); } endInsertRows(); Q_EMIT accountAdded(account); Q_EMIT accountsChanged(); connect(account, &Account::identityChanged, this, [this, account]() { childIdentityChanged(account); account->writeToSettings(); }); if (!skipAuthenticationCheck) { connect(account, &Account::authenticated, this, [this, acctIndex](const bool authenticated, const QString &errorMessage) { if (authenticated) { m_accountStatus[acctIndex] = AccountStatus::Loaded; } else { m_accountStatus[acctIndex] = AccountStatus::InvalidCredentials; m_accountStatusStrings[acctIndex] = errorMessage; } Q_EMIT dataChanged(index(0, 0), index(m_accounts.size() - 1, 0)); }); } connect(account, &Account::fetchedTimeline, this, [this, account](const QString &original_name, QList posts) { Q_EMIT fetchedTimeline(account, original_name, std::move(posts)); }); connect(account, &Account::invalidated, this, [this, account]() { Q_EMIT invalidated(account); }); connect(account, &Account::fetchedInstanceMetadata, this, [this, account]() { Q_EMIT fetchedInstanceMetadata(account); Q_EMIT dataChanged(index(0, 0), index(m_accounts.size() - 1, 0)); }); connect(account, &Account::invalidatedPost, this, [this, account](Post *p) { Q_EMIT invalidatedPost(account, p); }); connect(account, &Account::notification, this, [this, account](std::shared_ptr n) { AccountManager::instance().notificationHandler()->handle(std::move(n), account); Q_EMIT notification(account, std::move(n)); }); if (m_selected_account == nullptr) { m_selected_account = account; Q_EMIT accountSelected(m_selected_account); } if (m_testMode) { checkIfLoadingFinished(); } } void AccountManager::childIdentityChanged(AbstractAccount *account) { Q_EMIT identityChanged(account); const auto idx = m_accounts.indexOf(account); Q_EMIT dataChanged(index(idx, 0), index(idx, 0)); } void AccountManager::removeAccount(AbstractAccount *account) { // remove from settings auto config = KSharedConfig::openStateConfig(); config->deleteGroup(account->settingsGroupName()); config->sync(); auto accessTokenJob = new QKeychain::DeletePasswordJob{QStringLiteral("Tokodon")}; accessTokenJob->setKey(account->accessTokenKey()); accessTokenJob->start(); auto clientSecretJob = new QKeychain::DeletePasswordJob{QStringLiteral("Tokodon")}; clientSecretJob->setKey(account->clientSecretKey()); clientSecretJob->start(); const auto index = m_accounts.indexOf(account); beginRemoveRows(QModelIndex(), index, index); m_accounts.removeOne(account); endRemoveRows(); if (hasAccounts()) { m_selected_account = m_accounts.first(); } else { m_selected_account = nullptr; } Q_EMIT accountSelected(m_selected_account); Q_EMIT accountRemoved(account); Q_EMIT accountsChanged(); } void AccountManager::reloadAccounts() { bool anyAccountsReloaded = false; for (auto account : std::as_const(m_accounts)) { if (account->haveToken()) { account->validateToken(); anyAccountsReloaded = true; } } if (anyAccountsReloaded) { Q_EMIT accountsReloaded(); } } bool AccountManager::selectedAccountHasIssue() const { if (!m_selected_account) { return false; } return accountHasIssue(m_selected_account); } bool AccountManager::accountHasIssue(AbstractAccount *account) const { if (!m_selected_account) { return false; } const int index = m_accounts.indexOf(account); if (index != -1 && index < m_accountStatus.size()) { return m_accountStatus[index] == AccountStatus::InvalidCredentials; } return false; } QString AccountManager::selectedAccountLoginIssue() const { if (!m_selected_account) { return {}; } const int index = m_accounts.indexOf(m_selected_account); if (index != -1) { return m_accountStatusStrings[index]; } return {}; } void AccountManager::selectAccount(AbstractAccount *account, bool explicitUserAction) { if (!m_accounts.contains(account)) { qDebug() << "WTF: attempt to select unmanaged account" << account; return; } m_selected_account = account; if (explicitUserAction && !testMode()) { auto config = Config::self(); config->setLastUsedAccount(account->settingsGroupName()); config->save(); } Q_EMIT accountSelected(account); } AbstractAccount *AccountManager::selectedAccount() const { return m_selected_account; } QString AccountManager::selectedAccountId() const { return m_selected_account->identity()->id(); } int AccountManager::selectedIndex() const { for (int i = 0; i < m_accounts.length(); i++) { if (m_selected_account == m_accounts[i]) { return i; } } return -1; } void AccountManager::loadFromSettings() { if (m_testMode) { qCDebug(TOKODON_LOG) << "Test mode enabled, no local accounts are loaded."; return; } qCDebug(TOKODON_LOG) << "Loading accounts from settings."; auto config = KSharedConfig::openStateConfig(); for (const auto &id : config->groupList()) { if (id.contains('@'_L1)) { auto accountConfig = new AccountConfig{id}; if (accountConfig->clientId().isEmpty() || accountConfig->instanceUri().isEmpty()) { accountConfig->deleteLater(); continue; } const int index = m_accountStatus.size(); m_accountStatus.push_back(AccountStatus::NotLoaded); m_accountStatusStrings.push_back({}); auto account = new Account(accountConfig, m_qnam); addAccount(account, true); connect(account, &Account::authenticated, this, [this, account, index](const bool successful, const QString &errorMessage) { if (successful && account->haveToken() && account->hasName() && account->hasInstanceUrl()) { m_accountStatus[index] = AccountStatus::Loaded; } else { m_accountStatus[index] = AccountStatus::InvalidCredentials; m_accountStatusStrings[index] = errorMessage; } checkIfLoadingFinished(); }); } } checkIfLoadingFinished(); } KAboutData AccountManager::aboutData() const { return m_aboutData; } void AccountManager::setAboutData(const KAboutData &aboutData) { m_aboutData = aboutData; Q_EMIT aboutDataChanged(); } void AccountManager::checkIfLoadingFinished() { // no accounts at all if (m_accountStatus.empty()) { m_ready = true; Q_EMIT accountsReady(); return; } bool finished = true; for (auto status : m_accountStatus) { if (status == AccountStatus::NotLoaded) finished = false; } if (!finished) { return; } qCDebug(TOKODON_LOG) << "Accounts have finished loading."; auto config = Config::self(); for (auto account : m_accounts) { // old LastUsedAccount values used to be only username const bool isOldVersion = !config->lastUsedAccount().contains(QLatin1Char('@')); const bool isEmpty = config->lastUsedAccount().isEmpty() || config->lastUsedAccount() == '@'_L1; const bool matchesNewFormat = account->settingsGroupName() == config->lastUsedAccount(); const bool matchesOldFormat = account->username() == config->lastUsedAccount(); const bool isValid = isEmpty || (isOldVersion ? matchesOldFormat : matchesNewFormat); if (isValid) { selectAccount(account, false); break; } } m_ready = true; Q_EMIT accountsReady(); } bool AccountManager::isReady() const { return m_ready; } QString AccountManager::settingsGroupName(const QString &name, const QString &instanceUri) { Q_ASSERT(!name.isEmpty()); Q_ASSERT(!instanceUri.isEmpty()); return QStringLiteral("%1@%2").arg(name, QUrl(instanceUri).host()); } QString AccountManager::clientSecretKey(const QString &name) { #ifdef TOKODON_FLATPAK return QStringLiteral("%1-flatpak-client-secret").arg(name); #else return QStringLiteral("%1-client-secret").arg(name); #endif } QString AccountManager::accessTokenKey(const QString &name) { #ifdef TOKODON_FLATPAK return QStringLiteral("%1-flatpak- client-secret").arg(name); #else return QStringLiteral("%1-access-token").arg(name); #endif } void AccountManager::migrateSettings() { if (m_testMode) { return; } QSettings settings; const auto version = settings.value("settingsVersion", -1).toInt(); if (version == 0) { qCDebug(TOKODON_LOG) << "Migrating v0 settings to v1"; settings.beginGroup("accounts"); const auto childGroups = settings.childGroups(); // we are just going to re-index qCDebug(TOKODON_LOG) << "Account list is" << childGroups; for (int i = 0; i < childGroups.size(); i++) { // we're going to move all of this into an array instead const auto child = childGroups[i]; settings.beginGroup(child); const auto keysInChild = settings.childKeys(); const auto childName = settings.value("name").toString(); const auto childInstance = QUrl(settings.value("instance_uri").toString()).host(); const QString newName = QStringLiteral("%1@%2").arg(childName, childInstance); qCDebug(TOKODON_LOG) << "Rewriting key from" << child << "to" << newName; settings.endGroup(); for (const auto &key : keysInChild) { settings.beginGroup(child); const auto value = settings.value(key); settings.endGroup(); // child settings.beginGroup(newName); settings.setValue(key, value); settings.endGroup(); } // after porting over the settings, remove it settings.remove(child); } settings.endGroup(); settings.setValue("settingsVersion", 1); // we need to migrate to kconfig migrateSettings(); } else if (version == 1) { qCDebug(TOKODON_LOG) << "Migrating v1 settings to kconfig"; settings.beginGroup("accounts"); const auto childGroups = settings.childGroups(); for (int i = 0; i < childGroups.size(); i++) { const auto child = childGroups[i]; settings.beginGroup(child); const auto childName = settings.value("name").toString(); const auto childInstance = QUrl(settings.value("instance_uri").toString()).host(); const QString settingsGroupName = QStringLiteral("%1@%2").arg(childName, childInstance); AccountConfig config(settingsGroupName); config.setClientId(settings.value("client_id").toString()); config.setInstanceUri(settings.value("instance_uri").toString()); config.setName(settings.value("name").toString()); config.setIgnoreSslErrors(settings.value("ignoreSslErrors").toBool()); config.save(); auto accessTokenJob = new QKeychain::WritePasswordJob{QStringLiteral("Tokodon")}; accessTokenJob->setKey(AccountManager::accessTokenKey(settingsGroupName)); accessTokenJob->setTextData(settings.value("token").toString()); accessTokenJob->start(); auto clientSecretJob = new QKeychain::WritePasswordJob{QStringLiteral("Tokodon")}; clientSecretJob->setKey(AccountManager::clientSecretKey(settingsGroupName)); clientSecretJob->setTextData(settings.value("client_secret").toString()); clientSecretJob->start(); settings.endGroup(); } settings.endGroup(); // wipe file settings.clear(); } } bool AccountManager::isFlatpak() const { #ifdef TOKODON_FLATPAK return true; #else return false; #endif } void AccountManager::setTestMode(const bool enabled) { m_testMode = enabled; } bool AccountManager::testMode() const { return m_testMode; } void AccountManager::queueNotifications() { static qsizetype accountsLeft = m_accounts.size(); static qsizetype totalNotifications = 0; const auto checkIfDone = [this]() { // If there's no more accounts left to check, and none of them have notifications then early exit if (accountsLeft <= 0 && totalNotifications <= 0) { Q_EMIT finishedNotificationQueue(); } }; for (auto account : m_accounts) { QUrl uri; uri = QUrl::fromUserInput(account->instanceUri()); uri.setPath(QStringLiteral("/api/v1/notifications")); AccountConfig config(account->settingsGroupName()); QUrlQuery urlQuery(uri); urlQuery.addQueryItem(QStringLiteral("limit"), QString::number(10)); if (!config.lastPushNotification().isEmpty()) { urlQuery.addQueryItem(QStringLiteral("since_id"), config.lastPushNotification()); } uri.setQuery(urlQuery); account->get( uri, true, this, [account, checkIfDone](QNetworkReply *reply) { const auto data = reply->readAll(); const auto doc = QJsonDocument::fromJson(data); if (!doc.isArray() || doc.array().isEmpty()) { accountsLeft--; checkIfDone(); return; } // We want to post the notifications in reverse order, because the way Plasma displays them. // If we post the newest notification, it would be buried by the older ones. auto notifications = doc.array().toVariantList(); std::reverse(notifications.begin(), notifications.end()); for (const auto ¬ification : notifications) { if (notification.canConvert()) { std::shared_ptr n = std::make_shared(account, notification.toJsonObject()); Q_EMIT account->notification(n); } } AccountConfig config(account->settingsGroupName()); config.setLastPushNotification(doc.array().first()["id"_L1].toString()); config.save(); totalNotifications = doc.array().size(); accountsLeft--; checkIfDone(); }, [checkIfDone](QNetworkReply *) { accountsLeft--; checkIfDone(); }); } } QList AccountManager::accounts() const { return m_accounts; } #include "moc_accountmanager.cpp"