2023-08-05 22:14:05 -04:00
|
|
|
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
2023-07-30 08:49:34 -04:00
|
|
|
#include "account.h"
|
|
|
|
|
|
|
|
#include <QEventLoop>
|
|
|
|
#include <QNetworkReply>
|
|
|
|
#include <QNetworkRequest>
|
2023-09-23 13:16:28 -04:00
|
|
|
#include <cotp.h>
|
2023-10-06 18:08:21 -04:00
|
|
|
#include <qcorocore.h>
|
2023-09-18 18:03:19 -04:00
|
|
|
#include <qt6keychain/keychain.h>
|
2023-07-30 08:49:34 -04:00
|
|
|
|
2023-10-11 13:03:23 -04:00
|
|
|
#include "astra_log.h"
|
2023-07-30 08:49:34 -04:00
|
|
|
#include "launchercore.h"
|
2023-10-08 18:02:02 -04:00
|
|
|
#include "utility.h"
|
2023-07-30 08:49:34 -04:00
|
|
|
|
|
|
|
Account::Account(LauncherCore &launcher, const QString &key, QObject *parent)
|
|
|
|
: QObject(parent)
|
|
|
|
, m_config(key)
|
|
|
|
, m_key(key)
|
|
|
|
, m_launcher(launcher)
|
|
|
|
{
|
|
|
|
fetchAvatar();
|
|
|
|
}
|
|
|
|
|
|
|
|
QString Account::uuid() const
|
|
|
|
{
|
|
|
|
return m_key;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString Account::name() const
|
|
|
|
{
|
|
|
|
return m_config.name();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Account::setName(const QString &name)
|
|
|
|
{
|
|
|
|
if (m_config.name() != name) {
|
|
|
|
m_config.setName(name);
|
|
|
|
m_config.save();
|
|
|
|
Q_EMIT nameChanged();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-18 22:36:33 -04:00
|
|
|
int Account::language() const
|
|
|
|
{
|
|
|
|
return m_config.language();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Account::setLanguage(const int value)
|
|
|
|
{
|
|
|
|
if (m_config.language() != value) {
|
|
|
|
m_config.setLanguage(value);
|
|
|
|
m_config.save();
|
|
|
|
Q_EMIT languageChanged();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-30 08:49:34 -04:00
|
|
|
QString Account::lodestoneId() const
|
|
|
|
{
|
|
|
|
return m_config.lodestoneId();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Account::setLodestoneId(const QString &id)
|
|
|
|
{
|
|
|
|
if (m_config.lodestoneId() != id) {
|
|
|
|
m_config.setLodestoneId(id);
|
|
|
|
m_config.save();
|
|
|
|
fetchAvatar();
|
|
|
|
Q_EMIT lodestoneIdChanged();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
QString Account::avatarUrl() const
|
|
|
|
{
|
|
|
|
return m_url.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Account::isSapphire() const
|
|
|
|
{
|
|
|
|
return m_config.isSapphire();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Account::setIsSapphire(bool value)
|
|
|
|
{
|
|
|
|
if (m_config.isSapphire() != value) {
|
|
|
|
m_config.setIsSapphire(value);
|
|
|
|
m_config.save();
|
|
|
|
Q_EMIT isSapphireChanged();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
QString Account::lobbyUrl() const
|
|
|
|
{
|
|
|
|
return m_config.lobbyUrl();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Account::setLobbyUrl(const QString &value)
|
|
|
|
{
|
|
|
|
if (m_config.lobbyUrl() != value) {
|
|
|
|
m_config.setLobbyUrl(value);
|
|
|
|
m_config.save();
|
|
|
|
Q_EMIT lobbyUrlChanged();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Account::rememberPassword() const
|
|
|
|
{
|
|
|
|
return m_config.rememberPassword();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Account::setRememberPassword(const bool value)
|
|
|
|
{
|
|
|
|
if (m_config.rememberPassword() != value) {
|
|
|
|
m_config.setRememberPassword(value);
|
|
|
|
m_config.save();
|
|
|
|
Q_EMIT rememberPasswordChanged();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Account::rememberOTP() const
|
|
|
|
{
|
|
|
|
return m_config.rememberOTP();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Account::setRememberOTP(const bool value)
|
|
|
|
{
|
|
|
|
if (m_config.rememberOTP() != value) {
|
|
|
|
m_config.setRememberOTP(value);
|
|
|
|
m_config.save();
|
|
|
|
Q_EMIT rememberOTPChanged();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Account::useOTP() const
|
|
|
|
{
|
|
|
|
return m_config.useOTP();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Account::setUseOTP(const bool value)
|
|
|
|
{
|
|
|
|
if (m_config.useOTP() != value) {
|
|
|
|
m_config.setUseOTP(value);
|
|
|
|
m_config.save();
|
|
|
|
Q_EMIT useOTPChanged();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Account::GameLicense Account::license() const
|
|
|
|
{
|
|
|
|
return static_cast<GameLicense>(m_config.license());
|
|
|
|
}
|
|
|
|
|
|
|
|
void Account::setLicense(const GameLicense license)
|
|
|
|
{
|
|
|
|
if (static_cast<GameLicense>(m_config.license()) != license) {
|
|
|
|
m_config.setLicense(static_cast<int>(license));
|
|
|
|
m_config.save();
|
|
|
|
Q_EMIT licenseChanged();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Account::isFreeTrial() const
|
|
|
|
{
|
|
|
|
return m_config.isFreeTrial();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Account::setIsFreeTrial(const bool value)
|
|
|
|
{
|
|
|
|
if (m_config.isFreeTrial() != value) {
|
|
|
|
m_config.setIsFreeTrial(value);
|
|
|
|
m_config.save();
|
|
|
|
Q_EMIT isFreeTrialChanged();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-30 10:11:14 -04:00
|
|
|
QString Account::getPassword()
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2023-10-06 18:08:21 -04:00
|
|
|
return QCoro::waitFor(getKeychainValue(QStringLiteral("password")));
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
void Account::setPassword(const QString &password)
|
|
|
|
{
|
2023-09-16 21:22:20 -04:00
|
|
|
setKeychainValue(QStringLiteral("password"), password);
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
|
|
|
|
2023-07-30 10:11:14 -04:00
|
|
|
QString Account::getOTP()
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2023-10-06 18:08:21 -04:00
|
|
|
auto otpSecret = QCoro::waitFor(getKeychainValue(QStringLiteral("otp-secret")));
|
|
|
|
if (otpSecret.isEmpty()) {
|
|
|
|
return {};
|
|
|
|
}
|
2023-07-30 08:49:34 -04:00
|
|
|
|
2023-10-06 18:08:21 -04:00
|
|
|
cotp_error err;
|
|
|
|
char *totp = get_totp(otpSecret.toStdString().c_str(), 6, 30, SHA1, &err);
|
2023-07-30 08:49:34 -04:00
|
|
|
|
2023-10-06 18:08:21 -04:00
|
|
|
if (err == NO_ERROR) {
|
2023-12-17 10:09:01 -05:00
|
|
|
QString totpStr = QString::fromLatin1(totp);
|
2023-10-06 18:08:21 -04:00
|
|
|
free(totp);
|
|
|
|
return totpStr;
|
|
|
|
} else {
|
|
|
|
return {};
|
|
|
|
}
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
|
|
|
|
2023-09-20 15:33:26 -04:00
|
|
|
void Account::setOTPSecret(const QString &secret)
|
|
|
|
{
|
|
|
|
setKeychainValue(QStringLiteral("otp-secret"), secret);
|
|
|
|
}
|
|
|
|
|
2023-08-18 14:52:06 -04:00
|
|
|
QDir Account::getConfigDir() const
|
|
|
|
{
|
|
|
|
const QDir dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
2023-09-16 21:22:20 -04:00
|
|
|
const QDir userDir = dataDir.absoluteFilePath(QStringLiteral("user"));
|
2023-08-18 14:52:06 -04:00
|
|
|
return userDir.absoluteFilePath(m_key);
|
|
|
|
}
|
|
|
|
|
2023-07-30 08:49:34 -04:00
|
|
|
void Account::fetchAvatar()
|
|
|
|
{
|
|
|
|
if (lodestoneId().isEmpty()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-09-16 21:22:20 -04:00
|
|
|
const auto cacheLocation = QStandardPaths::standardLocations(QStandardPaths::CacheLocation)[0] + QStringLiteral("/avatars");
|
2023-08-04 17:34:53 -04:00
|
|
|
if (!QDir().exists(cacheLocation)) {
|
|
|
|
QDir().mkpath(cacheLocation);
|
|
|
|
}
|
|
|
|
|
|
|
|
const QString filename(QStringLiteral("%1/%2.jpg").arg(cacheLocation, lodestoneId()));
|
|
|
|
if (!QFile(filename).exists()) {
|
|
|
|
qDebug() << "Did not find lodestone character " << lodestoneId() << " in cache, fetching from xivapi.";
|
2023-10-08 13:22:34 -04:00
|
|
|
|
|
|
|
QUrl url;
|
2023-10-11 13:25:24 -04:00
|
|
|
url.setScheme(m_launcher.settings()->preferredProtocol());
|
|
|
|
url.setHost(m_launcher.settings()->xivApiServer());
|
2023-10-08 13:22:34 -04:00
|
|
|
url.setPath(QStringLiteral("/character/%1").arg(lodestoneId()));
|
|
|
|
|
|
|
|
QNetworkRequest request(url);
|
2023-10-08 18:02:02 -04:00
|
|
|
Utility::printRequest(QStringLiteral("GET"), request);
|
|
|
|
|
2023-10-11 13:34:43 -04:00
|
|
|
const auto reply = m_launcher.mgr()->get(request);
|
2023-08-04 17:34:53 -04:00
|
|
|
connect(reply, &QNetworkReply::finished, [this, filename, reply] {
|
|
|
|
auto document = QJsonDocument::fromJson(reply->readAll());
|
|
|
|
if (document.isObject()) {
|
2023-12-17 10:09:01 -05:00
|
|
|
const QNetworkRequest avatarRequest(QUrl(document.object()[QLatin1String("Character")].toObject()[QLatin1String("Avatar")].toString()));
|
2023-10-08 18:02:02 -04:00
|
|
|
Utility::printRequest(QStringLiteral("GET"), avatarRequest);
|
|
|
|
|
2023-10-11 13:34:43 -04:00
|
|
|
auto avatarReply = m_launcher.mgr()->get(avatarRequest);
|
2023-08-04 17:34:53 -04:00
|
|
|
QObject::connect(avatarReply, &QNetworkReply::finished, [this, filename, avatarReply] {
|
|
|
|
QFile file(filename);
|
|
|
|
file.open(QIODevice::ReadWrite);
|
|
|
|
file.write(avatarReply->readAll());
|
|
|
|
file.close();
|
|
|
|
|
2023-12-17 10:09:01 -05:00
|
|
|
m_url = QUrl(QStringLiteral("file:///%1").arg(filename));
|
2023-08-04 17:34:53 -04:00
|
|
|
Q_EMIT avatarUrlChanged();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
2023-12-17 10:09:01 -05:00
|
|
|
m_url = QUrl(QStringLiteral("file:///%1").arg(filename));
|
2023-08-04 17:34:53 -04:00
|
|
|
Q_EMIT avatarUrlChanged();
|
|
|
|
}
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
|
|
|
|
2023-07-30 10:11:14 -04:00
|
|
|
void Account::setKeychainValue(const QString &key, const QString &value)
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2023-09-16 21:22:20 -04:00
|
|
|
auto job = new QKeychain::WritePasswordJob(QStringLiteral("Astra"), this);
|
2023-07-30 08:49:34 -04:00
|
|
|
job->setTextData(value);
|
2023-10-08 18:20:13 -04:00
|
|
|
#ifdef FLATPAK
|
|
|
|
job->setKey(QStringLiteral("flatpak-") + m_key + QStringLiteral("-") + key);
|
|
|
|
#else
|
2023-09-16 21:22:20 -04:00
|
|
|
job->setKey(m_key + QStringLiteral("-") + key);
|
2023-10-08 18:20:13 -04:00
|
|
|
#endif
|
2023-08-18 22:42:24 -04:00
|
|
|
job->setInsecureFallback(m_launcher.isSteamDeck()); // The Steam Deck does not have secrets provider in Game Mode
|
2023-07-30 08:49:34 -04:00
|
|
|
job->start();
|
|
|
|
}
|
|
|
|
|
2023-10-06 18:08:21 -04:00
|
|
|
QCoro::Task<QString> Account::getKeychainValue(const QString &key)
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2023-09-16 21:22:20 -04:00
|
|
|
auto job = new QKeychain::ReadPasswordJob(QStringLiteral("Astra"), this);
|
2023-10-08 18:20:13 -04:00
|
|
|
#ifdef FLATPAK
|
|
|
|
job->setKey(QStringLiteral("flatpak-") + m_key + QStringLiteral("-") + key);
|
|
|
|
#else
|
2023-09-16 21:22:20 -04:00
|
|
|
job->setKey(m_key + QStringLiteral("-") + key);
|
2023-10-08 18:20:13 -04:00
|
|
|
#endif
|
2023-08-18 22:42:24 -04:00
|
|
|
job->setInsecureFallback(m_launcher.isSteamDeck());
|
2023-07-30 08:49:34 -04:00
|
|
|
job->start();
|
|
|
|
|
2023-10-06 18:08:21 -04:00
|
|
|
co_await qCoro(job, &QKeychain::ReadPasswordJob::finished);
|
2023-07-30 08:49:34 -04:00
|
|
|
|
2023-10-06 18:08:21 -04:00
|
|
|
co_return job->textData();
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
2023-10-11 13:03:23 -04:00
|
|
|
|
|
|
|
void Account::updateConfig()
|
|
|
|
{
|
2023-12-17 10:09:01 -05:00
|
|
|
auto configDir = getConfigDir().absoluteFilePath(QStringLiteral("FFXIV.cfg"));
|
2023-10-11 13:03:23 -04:00
|
|
|
|
|
|
|
if (!QFile::exists(configDir)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
qInfo(ASTRA_LOG) << "Updating FFXIV.cfg...";
|
|
|
|
|
|
|
|
auto configDirStd = configDir.toStdString();
|
|
|
|
|
|
|
|
auto cfgFileBuffer = physis_read_file(configDirStd.c_str());
|
|
|
|
auto cfgFile = physis_cfg_parse(cfgFileBuffer);
|
|
|
|
|
|
|
|
// Ensure that the opening cutscene movie never plays, since it's broken in most versions of Wine
|
|
|
|
physis_cfg_set_value(cfgFile, "CutsceneMovieOpening", "1");
|
|
|
|
|
2023-10-11 13:25:24 -04:00
|
|
|
auto screenshotDir = m_launcher.settings()->screenshotDir();
|
2023-10-11 13:03:23 -04:00
|
|
|
|
|
|
|
if (!QDir().exists(screenshotDir))
|
|
|
|
QDir().mkpath(screenshotDir);
|
|
|
|
|
|
|
|
auto screenshotDirWin = Utility::toWindowsPath(screenshotDir);
|
|
|
|
auto screenshotDirWinStd = screenshotDirWin.toStdString();
|
|
|
|
|
|
|
|
// Set the screenshot path
|
|
|
|
physis_cfg_set_value(cfgFile, "ScreenShotDir", screenshotDirWinStd.c_str());
|
|
|
|
|
|
|
|
auto buffer = physis_cfg_write(cfgFile);
|
|
|
|
|
|
|
|
QFile file(configDir);
|
|
|
|
file.open(QIODevice::WriteOnly);
|
|
|
|
file.write(reinterpret_cast<const char *>(buffer.data), buffer.size);
|
|
|
|
file.close();
|
|
|
|
}
|