File indexing completed on 2024-05-12 05:04:08

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 
0007 #include "account.h"
0008 #include "config.h"
0009 #include "networkaccessmanagerfactory.h"
0010 #include "tokodon_debug.h"
0011 
0012 #include <qt6keychain/keychain.h>
0013 
0014 using namespace Qt::Literals::StringLiterals;
0015 
0016 AccountManager::AccountManager(QObject *parent)
0017     : QAbstractListModel(parent)
0018     , m_selected_account(nullptr)
0019     , m_qnam(NetworkAccessManagerFactory().create(this))
0020 {
0021 }
0022 
0023 AccountManager::~AccountManager()
0024 {
0025     for (auto a : std::as_const(m_accounts)) {
0026         delete a;
0027     }
0028 
0029     m_accounts.clear();
0030 }
0031 
0032 QVariant AccountManager::data(const QModelIndex &index, int role) const
0033 {
0034     if (!index.isValid()) {
0035         return {};
0036     }
0037 
0038     auto account = m_accounts.at(index.row());
0039     if (account->identity() == nullptr) {
0040         return {};
0041     }
0042     switch (role) {
0043     case Qt::DisplayRole:
0044     case DisplayNameRole:
0045         if (!account->identity()->displayNameHtml().isEmpty())
0046             return account->identity()->displayNameHtml();
0047         else
0048             return account->username();
0049     case DescriptionRole:
0050         return account->identity()->account();
0051     case InstanceRole:
0052         return account->instanceName();
0053     case AccountRole:
0054         return QVariant::fromValue(m_accounts[index.row()]);
0055     }
0056     return {};
0057 }
0058 
0059 int AccountManager::rowCount(const QModelIndex &index) const
0060 {
0061     Q_UNUSED(index)
0062     return m_accounts.size();
0063 }
0064 
0065 QHash<int, QByteArray> AccountManager::roleNames() const
0066 {
0067     return {
0068         {Qt::DisplayRole, QByteArrayLiteral("display")},
0069         {DisplayNameRole, QByteArrayLiteral("displayName")},
0070         {AccountRole, QByteArrayLiteral("account")},
0071         {DescriptionRole, QByteArrayLiteral("description")},
0072         {InstanceRole, QByteArrayLiteral("instance")},
0073     };
0074 }
0075 
0076 AccountManager &AccountManager::instance()
0077 {
0078     static AccountManager accountManager;
0079     return accountManager;
0080 }
0081 
0082 AbstractAccount *AccountManager::createNewAccount(const QString &instanceUri, bool ignoreSslErrors, bool admin)
0083 {
0084     return new Account(instanceUri, m_qnam, ignoreSslErrors, admin, this);
0085 }
0086 
0087 bool AccountManager::hasAccounts() const
0088 {
0089     return !m_accounts.empty();
0090 }
0091 
0092 bool AccountManager::hasAnyAccounts() const
0093 {
0094     return m_hasAnyAccounts;
0095 }
0096 
0097 void AccountManager::addAccount(AbstractAccount *account, bool skipAuthenticationCheck)
0098 {
0099     beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size());
0100     m_accounts.append(account);
0101     int acctIndex = m_accountStatus.size();
0102     if (!skipAuthenticationCheck) {
0103         if (m_testMode) {
0104             m_accountStatus.push_back(AccountStatus::Loaded);
0105         } else {
0106             m_accountStatus.push_back(AccountStatus::NotLoaded);
0107         }
0108         m_accountStatusStrings.push_back({});
0109     }
0110     endInsertRows();
0111 
0112     Q_EMIT accountAdded(account);
0113     Q_EMIT accountsChanged();
0114     connect(account, &Account::identityChanged, this, [this, account]() {
0115         childIdentityChanged(account);
0116         account->writeToSettings();
0117     });
0118     if (!skipAuthenticationCheck) {
0119         connect(account, &Account::authenticated, this, [this, acctIndex](const bool authenticated, const QString &errorMessage) {
0120             if (authenticated) {
0121                 m_accountStatus[acctIndex] = AccountStatus::Loaded;
0122             } else {
0123                 m_accountStatus[acctIndex] = AccountStatus::InvalidCredentials;
0124                 m_accountStatusStrings[acctIndex] = errorMessage;
0125             }
0126             Q_EMIT dataChanged(index(0, 0), index(m_accounts.size() - 1, 0));
0127         });
0128     }
0129 
0130     connect(account, &Account::fetchedTimeline, this, [this, account](const QString &original_name, QList<Post *> posts) {
0131         Q_EMIT fetchedTimeline(account, original_name, std::move(posts));
0132     });
0133     connect(account, &Account::invalidated, this, [this, account]() {
0134         Q_EMIT invalidated(account);
0135     });
0136     connect(account, &Account::fetchedInstanceMetadata, this, [this, account]() {
0137         Q_EMIT fetchedInstanceMetadata(account);
0138         Q_EMIT dataChanged(index(0, 0), index(m_accounts.size() - 1, 0));
0139     });
0140     connect(account, &Account::invalidatedPost, this, [this, account](Post *p) {
0141         Q_EMIT invalidatedPost(account, p);
0142     });
0143     connect(account, &Account::notification, this, [this, account](std::shared_ptr<Notification> n) {
0144         Q_EMIT notification(account, std::move(n));
0145     });
0146 
0147     if (m_selected_account == nullptr) {
0148         m_selected_account = account;
0149         Q_EMIT accountSelected(m_selected_account);
0150     }
0151 
0152     if (m_testMode) {
0153         checkIfLoadingFinished();
0154     }
0155 }
0156 
0157 void AccountManager::childIdentityChanged(AbstractAccount *account)
0158 {
0159     Q_EMIT identityChanged(account);
0160 
0161     const auto idx = m_accounts.indexOf(account);
0162     Q_EMIT dataChanged(index(idx, 0), index(idx, 0));
0163 }
0164 
0165 void AccountManager::removeAccount(AbstractAccount *account)
0166 {
0167     // remove from settings
0168     auto config = KSharedConfig::openStateConfig();
0169     config->deleteGroup(account->settingsGroupName());
0170     config->sync();
0171 
0172     auto accessTokenJob = new QKeychain::DeletePasswordJob{QStringLiteral("Tokodon")};
0173     accessTokenJob->setKey(account->accessTokenKey());
0174     accessTokenJob->start();
0175 
0176     auto clientSecretJob = new QKeychain::DeletePasswordJob{QStringLiteral("Tokodon")};
0177     clientSecretJob->setKey(account->clientSecretKey());
0178     clientSecretJob->start();
0179 
0180     const auto index = m_accounts.indexOf(account);
0181     beginRemoveRows(QModelIndex(), index, index);
0182     m_accounts.removeOne(account);
0183     endRemoveRows();
0184 
0185     if (hasAccounts()) {
0186         m_selected_account = m_accounts[0];
0187     } else {
0188         m_selected_account = nullptr;
0189     }
0190     Q_EMIT accountSelected(m_selected_account);
0191 
0192     Q_EMIT accountRemoved(account);
0193     Q_EMIT accountsChanged();
0194 }
0195 
0196 void AccountManager::reloadAccounts()
0197 {
0198     for (auto account : std::as_const(m_accounts)) {
0199         if (account->haveToken()) {
0200             account->validateToken();
0201         }
0202     }
0203 
0204     Q_EMIT accountsReloaded();
0205 }
0206 
0207 bool AccountManager::selectedAccountHasIssue() const
0208 {
0209     if (!m_selected_account) {
0210         return false;
0211     }
0212 
0213     const int index = m_accounts.indexOf(m_selected_account);
0214     if (index != -1 && index < m_accountStatus.size()) {
0215         return m_accountStatus[index] == AccountStatus::InvalidCredentials;
0216     }
0217 
0218     return false;
0219 }
0220 
0221 QString AccountManager::selectedAccountLoginIssue() const
0222 {
0223     if (!m_selected_account) {
0224         return {};
0225     }
0226 
0227     const int index = m_accounts.indexOf(m_selected_account);
0228     if (index != -1) {
0229         return m_accountStatusStrings[index];
0230     }
0231 
0232     return {};
0233 }
0234 
0235 void AccountManager::selectAccount(AbstractAccount *account, bool explicitUserAction)
0236 {
0237     if (!m_accounts.contains(account)) {
0238         qDebug() << "WTF: attempt to select unmanaged account" << account;
0239         return;
0240     }
0241 
0242     m_selected_account = account;
0243 
0244     if (explicitUserAction) {
0245         auto config = Config::self();
0246         config->setLastUsedAccount(account->settingsGroupName());
0247         config->save();
0248     }
0249 
0250     Q_EMIT accountSelected(account);
0251 }
0252 
0253 AbstractAccount *AccountManager::selectedAccount() const
0254 {
0255     return m_selected_account;
0256 }
0257 
0258 QString AccountManager::selectedAccountId() const
0259 {
0260     return m_selected_account->identity()->id();
0261 }
0262 
0263 int AccountManager::selectedIndex() const
0264 {
0265     for (int i = 0; i < m_accounts.length(); i++) {
0266         if (m_selected_account == m_accounts[i]) {
0267             return i;
0268         }
0269     }
0270     return -1;
0271 }
0272 
0273 void AccountManager::loadFromSettings()
0274 {
0275     if (m_testMode) {
0276         qCDebug(TOKODON_LOG) << "Test mode enabled, no local accounts are loaded.";
0277         return;
0278     }
0279 
0280     qCDebug(TOKODON_LOG) << "Loading accounts from settings.";
0281 
0282     auto config = KSharedConfig::openStateConfig();
0283     for (const auto &id : config->groupList()) {
0284         if (id.contains('@'_L1)) {
0285             auto accountConfig = new AccountConfig{id};
0286 
0287             if (accountConfig->clientId().isEmpty() || accountConfig->instanceUri().isEmpty()) {
0288                 accountConfig->deleteLater();
0289                 continue;
0290             }
0291 
0292             const int index = m_accountStatus.size();
0293             m_accountStatus.push_back(AccountStatus::NotLoaded);
0294             m_accountStatusStrings.push_back({});
0295 
0296             auto account = new Account(accountConfig, m_qnam);
0297             addAccount(account, true);
0298 
0299             connect(account, &Account::authenticated, this, [this, account, index](const bool successful, const QString &errorMessage) {
0300                 if (successful && account->haveToken() && account->hasName() && account->hasInstanceUrl()) {
0301                     m_accountStatus[index] = AccountStatus::Loaded;
0302                 } else {
0303                     m_accountStatus[index] = AccountStatus::InvalidCredentials;
0304                     m_accountStatusStrings[index] = errorMessage;
0305                 }
0306 
0307                 checkIfLoadingFinished();
0308             });
0309         }
0310     }
0311 
0312     checkIfLoadingFinished();
0313 }
0314 
0315 KAboutData AccountManager::aboutData() const
0316 {
0317     return m_aboutData;
0318 }
0319 
0320 void AccountManager::setAboutData(const KAboutData &aboutData)
0321 {
0322     m_aboutData = aboutData;
0323     Q_EMIT aboutDataChanged();
0324 }
0325 
0326 void AccountManager::checkIfLoadingFinished()
0327 {
0328     // no accounts at all
0329     if (m_accountStatus.empty()) {
0330         m_ready = true;
0331         Q_EMIT accountsReady();
0332         return;
0333     }
0334 
0335     bool finished = true;
0336     for (auto status : m_accountStatus) {
0337         if (status == AccountStatus::NotLoaded)
0338             finished = false;
0339     }
0340 
0341     if (!finished) {
0342         return;
0343     }
0344 
0345     qCDebug(TOKODON_LOG) << "Accounts have finished loading.";
0346 
0347     auto config = Config::self();
0348 
0349     for (auto account : m_accounts) {
0350         // old LastUsedAccount values used to be only username
0351         const bool isOldVersion = !config->lastUsedAccount().contains(QLatin1Char('@'));
0352         const bool isEmpty = config->lastUsedAccount().isEmpty() || config->lastUsedAccount() == '@'_L1;
0353         const bool matchesNewFormat = account->settingsGroupName() == config->lastUsedAccount();
0354         const bool matchesOldFormat = account->username() == config->lastUsedAccount();
0355 
0356         const bool isValid = isEmpty || (isOldVersion ? matchesOldFormat : matchesNewFormat);
0357 
0358         if (isValid) {
0359             selectAccount(account, false);
0360             break;
0361         }
0362     }
0363 
0364     m_ready = true;
0365     Q_EMIT accountsReady();
0366 }
0367 
0368 bool AccountManager::isReady() const
0369 {
0370     return m_ready;
0371 }
0372 
0373 QString AccountManager::settingsGroupName(const QString &name, const QString &instanceUri)
0374 {
0375     return name + QLatin1Char('@') + QUrl(instanceUri).host();
0376 }
0377 
0378 QString AccountManager::clientSecretKey(const QString &name)
0379 {
0380 #ifdef TOKODON_FLATPAK
0381     return QStringLiteral("%1-flatpak-client-secret").arg(name);
0382 #else
0383     return QStringLiteral("%1-client-secret").arg(name);
0384 #endif
0385 }
0386 
0387 QString AccountManager::accessTokenKey(const QString &name)
0388 {
0389 #ifdef TOKODON_FLATPAK
0390     return QStringLiteral("%1-flatpak-  client-secret").arg(name);
0391 #else
0392     return QStringLiteral("%1-access-token").arg(name);
0393 #endif
0394 }
0395 
0396 void AccountManager::migrateSettings()
0397 {
0398     if (m_testMode) {
0399         return;
0400     }
0401 
0402     QSettings settings;
0403 
0404     const auto version = settings.value("settingsVersion", -1).toInt();
0405     if (version == 0) {
0406         qCDebug(TOKODON_LOG) << "Migrating v0 settings to v1";
0407         settings.beginGroup("accounts");
0408         const auto childGroups = settings.childGroups();
0409         // we are just going to re-index
0410         qCDebug(TOKODON_LOG) << "Account list is" << childGroups;
0411         for (int i = 0; i < childGroups.size(); i++) {
0412             // we're going to move all of this into an array instead
0413             const auto child = childGroups[i];
0414             settings.beginGroup(child);
0415             const auto keysInChild = settings.childKeys();
0416             const auto childName = settings.value("name").toString();
0417             const auto childInstance = QUrl(settings.value("instance_uri").toString()).host();
0418             const QString newName = childName + QLatin1Char('@') + childInstance;
0419             qCDebug(TOKODON_LOG) << "Rewriting key from" << child << "to" << newName;
0420             settings.endGroup();
0421             for (const auto &key : keysInChild) {
0422                 settings.beginGroup(child);
0423                 const auto value = settings.value(key);
0424                 settings.endGroup(); // child
0425                 settings.beginGroup(newName);
0426                 settings.setValue(key, value);
0427                 settings.endGroup();
0428             }
0429             // after porting over the settings, remove it
0430             settings.remove(child);
0431         }
0432         settings.endGroup();
0433         settings.setValue("settingsVersion", 1);
0434 
0435         // we need to migrate to kconfig
0436         migrateSettings();
0437     } else if (version == 1) {
0438         qCDebug(TOKODON_LOG) << "Migrating v1 settings to kconfig";
0439 
0440         settings.beginGroup("accounts");
0441         const auto childGroups = settings.childGroups();
0442         for (int i = 0; i < childGroups.size(); i++) {
0443             const auto child = childGroups[i];
0444             settings.beginGroup(child);
0445 
0446             const auto childName = settings.value("name").toString();
0447             const auto childInstance = QUrl(settings.value("instance_uri").toString()).host();
0448 
0449             const QString settingsGroupName = childName + QLatin1Char('@') + childInstance;
0450 
0451             AccountConfig config(settingsGroupName);
0452             config.setClientId(settings.value("client_id").toString());
0453             config.setInstanceUri(settings.value("instance_uri").toString());
0454             config.setName(settings.value("name").toString());
0455             config.setIgnoreSslErrors(settings.value("ignoreSslErrors").toBool());
0456 
0457             config.save();
0458 
0459             auto accessTokenJob = new QKeychain::WritePasswordJob{QStringLiteral("Tokodon")};
0460             accessTokenJob->setKey(AccountManager::accessTokenKey(settingsGroupName));
0461             accessTokenJob->setTextData(settings.value("token").toString());
0462             accessTokenJob->start();
0463 
0464             auto clientSecretJob = new QKeychain::WritePasswordJob{QStringLiteral("Tokodon")};
0465             clientSecretJob->setKey(AccountManager::clientSecretKey(settingsGroupName));
0466             clientSecretJob->setTextData(settings.value("client_secret").toString());
0467             clientSecretJob->start();
0468 
0469             settings.endGroup();
0470         }
0471 
0472         settings.endGroup();
0473 
0474         // wipe file
0475         settings.clear();
0476     }
0477 }
0478 
0479 bool AccountManager::isFlatpak() const
0480 {
0481 #ifdef TOKODON_FLATPAK
0482     return true;
0483 #else
0484     return false;
0485 #endif
0486 }
0487 
0488 void AccountManager::setTestMode(const bool enabled)
0489 {
0490     m_testMode = enabled;
0491 }
0492 
0493 bool AccountManager::testMode() const
0494 {
0495     return m_testMode;
0496 }
0497 
0498 void AccountManager::queueNotifications()
0499 {
0500     static int accountsLeft = m_accounts.size();
0501 
0502     const auto checkIfDone = [this]() {
0503         qInfo() << "Accounts left to check:" << accountsLeft;
0504         if (accountsLeft <= 0) {
0505             Q_EMIT finishedNotificationQueue();
0506         }
0507     };
0508 
0509     for (auto account : m_accounts) {
0510         QUrl uri;
0511         uri = QUrl::fromUserInput(account->instanceUri());
0512         uri.setPath(QStringLiteral("/api/v1/notifications"));
0513 
0514         AccountConfig config(account->settingsGroupName());
0515 
0516         QUrlQuery urlQuery(uri);
0517         urlQuery.addQueryItem(QStringLiteral("limit"), QString::number(10));
0518         if (!config.lastPushNotification().isEmpty()) {
0519             urlQuery.addQueryItem(QStringLiteral("min_id"), config.lastPushNotification());
0520         }
0521         uri.setQuery(urlQuery);
0522 
0523         account->get(
0524             uri,
0525             true,
0526             this,
0527             [account, checkIfDone](QNetworkReply *reply) {
0528                 const auto data = reply->readAll();
0529                 const auto doc = QJsonDocument::fromJson(data);
0530 
0531                 if (!doc.isArray() || doc.array().isEmpty()) {
0532                     accountsLeft--;
0533                     checkIfDone();
0534                     return;
0535                 }
0536 
0537                 for (auto notification : doc.array()) {
0538                     if (notification.isObject()) {
0539                         std::shared_ptr<Notification> n = std::make_shared<Notification>(account, notification.toObject());
0540                         Q_EMIT account->notification(n);
0541                     }
0542                 }
0543 
0544                 AccountConfig config(account->settingsGroupName());
0545                 config.setLastPushNotification(doc.array().first()["id"_L1].toString());
0546                 config.save();
0547 
0548                 accountsLeft--;
0549                 checkIfDone();
0550             },
0551             [checkIfDone](QNetworkReply *) {
0552                 accountsLeft--;
0553                 checkIfDone();
0554             });
0555     }
0556 }
0557 
0558 #include "moc_accountmanager.cpp"