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"