File indexing completed on 2024-05-12 16:28:04

0001 // SPDX-FileCopyrightText: 2021 kaniini <https://git.pleroma.social/kaniini>
0002 // SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
0003 // SPDX-License-Identifier: GPL-3.0-only
0004 
0005 #include "accountmanager.h"
0006 #include "account.h"
0007 #include "accountconfig.h"
0008 #include "config.h"
0009 #include "network/networkaccessmanagerfactory.h"
0010 #include "tokodon_debug.h"
0011 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0012 #include <qt5keychain/keychain.h>
0013 #else
0014 #include <qt6keychain/keychain.h>
0015 #endif
0016 
0017 AccountManager::AccountManager(QObject *parent)
0018     : QAbstractListModel(parent)
0019     , m_selected_account(nullptr)
0020     , m_qnam(NetworkAccessManagerFactory().create(this))
0021 {
0022     migrateSettings();
0023     loadFromSettings();
0024 
0025     connect(this, &AccountManager::accountSelected, this, [=](AbstractAccount *account) {
0026         if (account != nullptr) {
0027             account->checkForFollowRequests();
0028         }
0029     });
0030 }
0031 
0032 AccountManager::~AccountManager()
0033 {
0034     for (auto a : std::as_const(m_accounts)) {
0035         delete a;
0036     }
0037 
0038     m_accounts.clear();
0039 }
0040 
0041 QVariant AccountManager::data(const QModelIndex &index, int role) const
0042 {
0043     if (!index.isValid()) {
0044         return {};
0045     }
0046 
0047     auto account = m_accounts.at(index.row());
0048     if (account->identity() == nullptr) {
0049         return {};
0050     }
0051     switch (role) {
0052     case Qt::DisplayRole:
0053     case DisplayNameRole:
0054         return account->identity()->displayNameHtml();
0055     case DescriptionRole:
0056         return account->identity()->account();
0057     case InstanceRole:
0058         return account->instanceName();
0059     case AccountRole:
0060         return QVariant::fromValue(m_accounts[index.row()]);
0061     }
0062     return {};
0063 }
0064 
0065 int AccountManager::rowCount(const QModelIndex &index) const
0066 {
0067     Q_UNUSED(index)
0068     return m_accounts.size();
0069 }
0070 
0071 QHash<int, QByteArray> AccountManager::roleNames() const
0072 {
0073     return {
0074         {Qt::DisplayRole, QByteArrayLiteral("display")},
0075         {DisplayNameRole, QByteArrayLiteral("displayName")},
0076         {AccountRole, QByteArrayLiteral("account")},
0077         {DescriptionRole, QByteArrayLiteral("description")},
0078         {InstanceRole, QByteArrayLiteral("instance")},
0079     };
0080 }
0081 
0082 AccountManager &AccountManager::instance()
0083 {
0084     static AccountManager accountManager;
0085     return accountManager;
0086 }
0087 
0088 AbstractAccount *AccountManager::createNewAccount(const QString &instanceUri, bool ignoreSslErrors)
0089 {
0090     return new Account(instanceUri, m_qnam, ignoreSslErrors, this);
0091 }
0092 
0093 bool AccountManager::hasAccounts() const
0094 {
0095     return !m_accounts.empty();
0096 }
0097 
0098 bool AccountManager::hasAnyAccounts() const
0099 {
0100     return m_hasAnyAccounts;
0101 }
0102 
0103 void AccountManager::addAccount(AbstractAccount *account)
0104 {
0105     beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size());
0106     m_accounts.append(account);
0107     endInsertRows();
0108 
0109     Q_EMIT accountAdded(account);
0110     Q_EMIT accountsChanged();
0111     connect(account, &Account::identityChanged, this, [this, account]() {
0112         childIdentityChanged(account);
0113         account->writeToSettings();
0114     });
0115     connect(account, &Account::authenticated, this, [this]() {
0116         Q_EMIT dataChanged(index(0, 0), index(m_accounts.size() - 1, 0));
0117     });
0118 
0119     connect(account, &Account::fetchedTimeline, this, [this, account](QString original_name, QList<Post *> posts) {
0120         Q_EMIT fetchedTimeline(account, original_name, posts);
0121     });
0122     connect(account, &Account::invalidated, this, [this, account]() {
0123         Q_EMIT invalidated(account);
0124     });
0125     connect(account, &Account::fetchedInstanceMetadata, this, [this, account]() {
0126         Q_EMIT fetchedInstanceMetadata(account);
0127         Q_EMIT dataChanged(index(0, 0), index(m_accounts.size() - 1, 0));
0128     });
0129     connect(account, &Account::invalidatedPost, this, [this, account](Post *p) {
0130         Q_EMIT invalidatedPost(account, p);
0131     });
0132     connect(account, &Account::notification, this, [this, account](std::shared_ptr<Notification> n) {
0133         Q_EMIT notification(account, n);
0134     });
0135 
0136     if (m_selected_account == nullptr) {
0137         m_selected_account = account;
0138         Q_EMIT accountSelected(m_selected_account);
0139     }
0140 }
0141 
0142 void AccountManager::childIdentityChanged(AbstractAccount *account)
0143 {
0144     Q_EMIT identityChanged(account);
0145 
0146     const auto idx = m_accounts.indexOf(account);
0147     Q_EMIT dataChanged(index(idx, 0), index(idx, 0));
0148 }
0149 
0150 void AccountManager::removeAccount(AbstractAccount *account)
0151 {
0152     // remove from settings
0153     auto config = KSharedConfig::openStateConfig();
0154     config->deleteGroup(account->settingsGroupName());
0155     config->sync();
0156 
0157     auto accessTokenJob = new QKeychain::DeletePasswordJob{"Tokodon"};
0158     accessTokenJob->setKey(account->accessTokenKey());
0159     accessTokenJob->start();
0160 
0161     auto clientSecretJob = new QKeychain::DeletePasswordJob{"Tokodon"};
0162     clientSecretJob->setKey(account->clientSecretKey());
0163     clientSecretJob->start();
0164 
0165     const auto index = m_accounts.indexOf(account);
0166     beginRemoveRows(QModelIndex(), index, index);
0167     m_accounts.removeOne(account);
0168     endRemoveRows();
0169 
0170     if (hasAccounts()) {
0171         m_selected_account = m_accounts[0];
0172     } else {
0173         m_selected_account = nullptr;
0174     }
0175     Q_EMIT accountSelected(m_selected_account);
0176 
0177     Q_EMIT accountRemoved(account);
0178     Q_EMIT accountsChanged();
0179 }
0180 
0181 void AccountManager::reloadAccounts()
0182 {
0183     for (auto account : std::as_const(m_accounts)) {
0184         account->validateToken();
0185     }
0186 
0187     Q_EMIT accountsReloaded();
0188 }
0189 
0190 void AccountManager::selectAccount(AbstractAccount *account, bool explicitUserAction)
0191 {
0192     if (!m_accounts.contains(account)) {
0193         qDebug() << "WTF: attempt to select unmanaged account" << account;
0194         return;
0195     }
0196 
0197     m_selected_account = account;
0198 
0199     if (explicitUserAction) {
0200         auto config = Config::self();
0201         config->setLastUsedAccount(account->settingsGroupName());
0202         config->save();
0203     }
0204 
0205     Q_EMIT accountSelected(account);
0206 }
0207 
0208 AbstractAccount *AccountManager::selectedAccount() const
0209 {
0210     return m_selected_account;
0211 }
0212 
0213 QString AccountManager::selectedAccountId() const
0214 {
0215     return m_selected_account->identity()->id();
0216 }
0217 
0218 int AccountManager::selectedIndex() const
0219 {
0220     for (int i = 0; i < m_accounts.length(); i++) {
0221         if (m_selected_account == m_accounts[i]) {
0222             return i;
0223         }
0224     }
0225     return -1;
0226 }
0227 
0228 void AccountManager::loadFromSettings()
0229 {
0230     qCDebug(TOKODON_LOG) << "Loading accounts from settings.";
0231 
0232     auto config = KSharedConfig::openStateConfig();
0233     for (const auto &id : config->groupList()) {
0234         if (id.contains('@')) {
0235             auto accountConfig = AccountConfig{id};
0236 
0237             int index = m_accountStatus.size();
0238             m_accountStatus.push_back(AccountStatus::NotLoaded);
0239 
0240             auto account = new Account(accountConfig, m_qnam);
0241             connect(account, &Account::authenticated, this, [=](bool successful) {
0242                 if (successful && account->haveToken() && account->hasName() && account->hasInstanceUrl()) {
0243                     m_accountStatus[index] = AccountStatus::Loaded;
0244 
0245                     addAccount(account);
0246                 } else {
0247                     m_accountStatus[index] = AccountStatus::InvalidCredentials;
0248 
0249                     delete account;
0250                 }
0251 
0252                 checkIfLoadingFinished();
0253             });
0254         }
0255     }
0256 
0257     checkIfLoadingFinished();
0258 }
0259 
0260 KAboutData AccountManager::aboutData() const
0261 {
0262     return m_aboutData;
0263 }
0264 
0265 void AccountManager::setAboutData(const KAboutData &aboutData)
0266 {
0267     m_aboutData = aboutData;
0268     Q_EMIT aboutDataChanged();
0269 }
0270 
0271 void AccountManager::checkIfLoadingFinished()
0272 {
0273     // no accounts at all
0274     if (m_accountStatus.empty()) {
0275         m_ready = true;
0276         Q_EMIT accountsReady();
0277         return;
0278     }
0279 
0280     bool finished = true;
0281     for (auto status : m_accountStatus) {
0282         if (status == AccountStatus::NotLoaded)
0283             finished = false;
0284     }
0285 
0286     if (!finished) {
0287         return;
0288     }
0289 
0290     qCDebug(TOKODON_LOG) << "Accounts have finished loading.";
0291 
0292     auto config = Config::self();
0293 
0294     for (auto account : m_accounts) {
0295         // old LastUsedAccount values used to be only username
0296         const bool isOldVersion = !config->lastUsedAccount().contains(QLatin1Char('@'));
0297         const bool isEmpty = config->lastUsedAccount().isEmpty() || config->lastUsedAccount() == '@';
0298         const bool matchesNewFormat = account->settingsGroupName() == config->lastUsedAccount();
0299         const bool matchesOldFormat = account->username() == config->lastUsedAccount();
0300 
0301         const bool isValid = isEmpty || (isOldVersion ? matchesOldFormat : matchesNewFormat);
0302 
0303         if (isValid) {
0304             selectAccount(account, false);
0305             break;
0306         }
0307     }
0308 
0309     m_ready = true;
0310     Q_EMIT accountsReady();
0311 }
0312 
0313 bool AccountManager::isReady() const
0314 {
0315     return m_ready;
0316 }
0317 
0318 QString AccountManager::settingsGroupName(const QString &name, const QString &instanceUri)
0319 {
0320     return name + QLatin1Char('@') + QUrl(instanceUri).host();
0321 }
0322 
0323 QString AccountManager::clientSecretKey(const QString &name)
0324 {
0325 #ifdef TOKODON_FLATPAK
0326     return QStringLiteral("%1-flatpak-client-secret").arg(name);
0327 #else
0328     return QStringLiteral("%1-client-secret").arg(name);
0329 #endif
0330 }
0331 
0332 QString AccountManager::accessTokenKey(const QString &name)
0333 {
0334 #ifdef TOKODON_FLATPAK
0335     return QStringLiteral("%1-flatpak-  client-secret").arg(name);
0336 #else
0337     return QStringLiteral("%1-access-token").arg(name);
0338 #endif
0339 }
0340 
0341 void AccountManager::migrateSettings()
0342 {
0343     QSettings settings;
0344 
0345     const auto version = settings.value("settingsVersion", -1).toInt();
0346     if (version == 0) {
0347         qCDebug(TOKODON_LOG) << "Migrating v0 settings to v1";
0348         settings.beginGroup("accounts");
0349         const auto childGroups = settings.childGroups();
0350         // we are just going to re-index
0351         qCDebug(TOKODON_LOG) << "Account list is" << childGroups;
0352         for (int i = 0; i < childGroups.size(); i++) {
0353             // we're going to move all of this into an array instead
0354             const auto child = childGroups[i];
0355             settings.beginGroup(child);
0356             const auto keysInChild = settings.childKeys();
0357             const auto childName = settings.value("name").toString();
0358             const auto childInstance = QUrl(settings.value("instance_uri").toString()).host();
0359             const QString newName = childName + QLatin1Char('@') + childInstance;
0360             qCDebug(TOKODON_LOG) << "Rewriting key from" << child << "to" << newName;
0361             settings.endGroup();
0362             for (const auto &key : keysInChild) {
0363                 settings.beginGroup(child);
0364                 const auto value = settings.value(key);
0365                 settings.endGroup(); // child
0366                 settings.beginGroup(newName);
0367                 settings.setValue(key, value);
0368                 settings.endGroup();
0369             }
0370             // after porting over the settings, remove it
0371             settings.remove(child);
0372         }
0373         settings.endGroup();
0374         settings.setValue("settingsVersion", 1);
0375 
0376         // we need to migrate to kconfig
0377         migrateSettings();
0378     } else if (version == 1) {
0379         qCDebug(TOKODON_LOG) << "Migrating v1 settings to kconfig";
0380 
0381         settings.beginGroup("accounts");
0382         const auto childGroups = settings.childGroups();
0383         for (int i = 0; i < childGroups.size(); i++) {
0384             const auto child = childGroups[i];
0385             settings.beginGroup(child);
0386 
0387             const auto childName = settings.value("name").toString();
0388             const auto childInstance = QUrl(settings.value("instance_uri").toString()).host();
0389 
0390             const QString settingsGroupName = childName + QLatin1Char('@') + childInstance;
0391 
0392             AccountConfig config(settingsGroupName);
0393             config.setClientId(settings.value("client_id").toString());
0394             config.setInstanceUri(settings.value("instance_uri").toString());
0395             config.setName(settings.value("name").toString());
0396             config.setIgnoreSslErrors(settings.value("ignoreSslErrors").toBool());
0397 
0398             config.save();
0399 
0400             auto accessTokenJob = new QKeychain::WritePasswordJob{"Tokodon"};
0401             accessTokenJob->setKey(AccountManager::accessTokenKey(settingsGroupName));
0402             accessTokenJob->setTextData(settings.value("token").toString());
0403             accessTokenJob->start();
0404 
0405             auto clientSecretJob = new QKeychain::WritePasswordJob{"Tokodon"};
0406             clientSecretJob->setKey(AccountManager::clientSecretKey(settingsGroupName));
0407             clientSecretJob->setTextData(settings.value("client_secret").toString());
0408             clientSecretJob->start();
0409 
0410             settings.endGroup();
0411         }
0412 
0413         settings.endGroup();
0414 
0415         // wipe file
0416         settings.clear();
0417     }
0418 }