diff --git a/android/adb/AdbUtils.cpp b/android/adb/AdbUtils.cpp index abbb92d..2302e84 100644 --- a/android/adb/AdbUtils.cpp +++ b/android/adb/AdbUtils.cpp @@ -162,7 +162,7 @@ QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, int idx, bool scre QString("%1 -s %2 pull /sdcard/screen.png screen_step_%3.png").arg(adb, deviceId).arg(idx), QString("%1 -s %2 shell rm /sdcard/screen.png").arg(adb, deviceId) }; - for (const QString &cmd : shotCmds) { + for (const QString &cmd: shotCmds) { startProcess(cmd); // ошибки игнорируем для скриншота } } @@ -288,6 +288,17 @@ bool AdbUtils::inputText(const QString &deviceId, const QString &text) { return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; } +bool AdbUtils::hideKeyboard(const QString &deviceId) { + QProcess process; + process.start(adbPath(), { + "-s", deviceId, + "shell", "input", + "keyevent", "111" + }); + process.waitForFinished(); + return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; +} + bool AdbUtils::goBack(const QString &deviceId) { QProcess process; process.start(adbPath(), { diff --git a/android/adb/AdbUtils.h b/android/adb/AdbUtils.h index d9a6ec8..7e26b65 100644 --- a/android/adb/AdbUtils.h +++ b/android/adb/AdbUtils.h @@ -37,6 +37,8 @@ public: static bool inputText(const QString &deviceId, const QString &text); + static bool hideKeyboard(const QString &deviceId); + static bool goBack(const QString &deviceId); static QSet getListApps(const QString &deviceId); diff --git a/android/birbank/LoginAndCheckAccountsBirbankScript.cpp b/android/birbank/LoginAndCheckAccountsBirbankScript.cpp index d09b31d..340f842 100644 --- a/android/birbank/LoginAndCheckAccountsBirbankScript.cpp +++ b/android/birbank/LoginAndCheckAccountsBirbankScript.cpp @@ -25,68 +25,79 @@ void LoginAndCheckAccountsBirbankScript::doStart() { const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode); - if (false) { + if (true) { + // xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + // if (!xmlScreenParser.isHomeScreen()) { + // if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { + // m_error = "Не смогли дойти до домашней страницы"; + // qCritical() << m_error; + // emit finishedWithResult(m_error); + // return; + // } + // } + // xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - Node cardText = xmlScreenParser.findTextNode("2480"); - if (!cardText.isEmpty()) { - AdbUtils::makeTap(device.id, cardText.x(), cardText.y()); - QThread::msleep(2000); + Node closeBannerNode = xmlScreenParser.findButtonNode("Maraqlı deyil", false); + if (!closeBannerNode.isEmpty()) { + AdbUtils::makeTap(m_deviceId, closeBannerNode.x(), closeBannerNode.y()); + QThread::msleep(1000); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - Node cardTitleText = xmlScreenParser.findTextNode("2480"); - Node cardHistory = xmlScreenParser.findTextNode("История платежей"); - if (!cardTitleText.isEmpty() && !cardHistory.isEmpty()) { - QList transactions = xmlScreenParser.parsePositiveTransactions(); + } - QList savedTransactions = TransactionDAO::getTransactions(89); - for (TransactionInfo &transaction: transactions) { - bool isSaved = false; - for (TransactionInfo &savedTransaction: savedTransactions) { - if (savedTransaction.amount == transaction.amount - && savedTransaction.bankTime == transaction.bankTime) { - isSaved = true; - break; - } - } - if (!isSaved) { - TransactionInfo tr; - tr.accountId = 89; - tr.description = transaction.description; - tr.bankTime = transaction.bankTime; - tr.bankName = transaction.bankName; - tr.amount = transaction.amount; - tr.phone = transaction.phone; - tr.updateTime = DateUtils::getUtcNow(); - tr.fee = transaction.fee; - tr.name = transaction.name; - tr.bankTrExternalId = transaction.bankTrExternalId; - tr.status = TransactionStatus::Complete; - tr.type = TransactionType::Card; - tr.completeTime = transaction.completeTime; - tr.id = TransactionDAO::insertTransaction(tr); + if (xmlScreenParser.isHomeScreen()) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node cardText = xmlScreenParser.findTextNode("2480"); + if (!cardText.isEmpty()) { + AdbUtils::makeTap(m_deviceId, cardText.x(), cardText.y()); + QThread::msleep(2000); - if (tr.id == -1) { - qDebug() << "--- saveAndPostNewTransactionData failed"; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node payBtn = xmlScreenParser.findTextNode("Перевести"); + if (!payBtn.isEmpty()) { + AdbUtils::makeTap(m_deviceId, payBtn.x(), payBtn.y()); + QThread::msleep(1000); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node payByCardBtn = xmlScreenParser.findTextNode("На карту любого банка"); + if (!payByCardBtn.isEmpty()) { + AdbUtils::makeTap(m_deviceId, payByCardBtn.x(), payByCardBtn.y()); + QThread::msleep(1000); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node cardEditText = xmlScreenParser.findEditText("Номер карты"); + Node amountEditText = xmlScreenParser.findEditText("Сумма"); + if (!cardEditText.isEmpty() && !amountEditText.isEmpty()) { + AdbUtils::makeTap(m_deviceId, cardEditText.x(), cardEditText.y()); + QThread::msleep(200); + AdbUtils::inputText(m_deviceId, "1111222233334444"); + + AdbUtils::hideKeyboard(m_deviceId); + + AdbUtils::makeTap(m_deviceId, amountEditText.x(), amountEditText.y()); + QThread::msleep(200); + AdbUtils::inputText(m_deviceId, "10"); + + AdbUtils::hideKeyboard(m_deviceId); + + + // дальше оплата, но хз как } - AccountInfo account = AccountDAO::getAccountById(89); - postNewTransactionData(account, tr); - // if (!saveAndPostNewTransactionData(m_account, transaction, TransactionStatus::Complete)) { - // qDebug() << "--- saveAndPostNewTransactionData failed"; - // } + } else { + m_error = "Pay by card button not found: " + m_deviceId + " " + m_appCode; + qWarning() << m_error; + emit finishedWithResult(m_error); + return; } + } else { + m_error = "Pay button not found: " + m_deviceId + " " + m_appCode; + qWarning() << m_error; + emit finishedWithResult(m_error); + return; } - } else { - m_error = "Card page not found: " + m_deviceId + " " + m_appCode; - qWarning() << m_error; - emit finishedWithResult(m_error); - return; + } - } else { - m_error = "Card not found: " + m_deviceId + " " + m_appCode; - qWarning() << m_error; - emit finishedWithResult(m_error); - return; } } else { diff --git a/android/birbank/PayByCardBirbankScript.cpp b/android/birbank/PayByCardBirbankScript.cpp new file mode 100644 index 0000000..b5d371f --- /dev/null +++ b/android/birbank/PayByCardBirbankScript.cpp @@ -0,0 +1,128 @@ +#include "PayByCardBirbankScript.h" + +#include +#include +#include + +#include "DatabaseManager.h" +#include "adb/AdbUtils.h" +#include "dao/AccountDAO.h" +#include "dao/ApplicationDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/TransactionDAO.h" +#include "db/ApplicationInfo.h" +#include "db/DeviceInfo.h" +#include "time/DateUtils.h" + +PayByCardBirbankScript::PayByCardBirbankScript( + AccountInfo account, + QString card, + QString fullName, + const double amount, + QObject *parent +) : CommonBirbankScript(parent), + m_account(std::move(account)), m_card(std::move(card)), + m_fullName(std::move(fullName)), m_amount(amount) { +} + +PayByCardBirbankScript::~PayByCardBirbankScript() = default; + + +void PayByCardBirbankScript::makePayment(const DeviceInfo &device) { + // Оплата со страницы карты + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter)); + + Node cardEditText = xmlScreenParser.findEditText("Номер карты"); + Node amountEditText = xmlScreenParser.findEditText("Сумма"); + if (!cardEditText.isEmpty() && !amountEditText.isEmpty()) { + AdbUtils::makeTap(device.id, cardEditText.x(), cardEditText.y()); + QThread::msleep(200); + AdbUtils::inputText(device.id, m_card); + + AdbUtils::hideKeyboard(device.id); + + AdbUtils::makeTap(device.id, amountEditText.x(), amountEditText.y()); + QThread::msleep(200); + AdbUtils::inputText(device.id, QString::number(m_amount)); + + AdbUtils::hideKeyboard(device.id); + + // дальше оплата, но хз как + + + } else { + m_error = "Не нашли поля ввода 'Номер карты' или 'Сумма'"; + qDebug() << m_error; + } +} + +void PayByCardBirbankScript::doStart() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId); + const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found."; + qDebug() << m_error; + emit finishedWithResult(m_error); + return; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); + if (!xmlScreenParser.isHomeScreen()) { + if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { + m_error = "Не смогли дойти до домашней страницы"; + qDebug() << m_error; + emit finishedWithResult(m_error); + return; + } + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); + Node closeBannerNode = xmlScreenParser.findButtonNode("Maraqlı deyil", false); + if (!closeBannerNode.isEmpty()) { + AdbUtils::makeTap(m_account.deviceId, closeBannerNode.x(), closeBannerNode.y()); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); + } + + if (xmlScreenParser.isHomeScreen()) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); + Node cardText = xmlScreenParser.findTextNode("2480"); + if (!cardText.isEmpty()) { + AdbUtils::makeTap(m_account.deviceId, cardText.x(), cardText.y()); + QThread::msleep(2000); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); + Node payBtn = xmlScreenParser.findTextNode("Перевести"); + if (!payBtn.isEmpty()) { + AdbUtils::makeTap(m_account.deviceId, payBtn.x(), payBtn.y()); + QThread::msleep(1000); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); + Node payByCardBtn = xmlScreenParser.findTextNode("На карту любого банка"); + if (!payByCardBtn.isEmpty()) { + AdbUtils::makeTap(m_account.deviceId, payByCardBtn.x(), payByCardBtn.y()); + QThread::msleep(1000); + + makePayment(device); + } else { + m_error = "Pay by card button not found: " + m_account.deviceId + " " + m_account.appCode; + qWarning() << m_error; + emit finishedWithResult(m_error); + return; + } + } else { + m_error = "Pay button not found: " + m_account.deviceId + " " + m_account.appCode; + qWarning() << m_error; + emit finishedWithResult(m_error); + return; + } + } + } + + if (!m_error.isEmpty()) { + AdbUtils::takeXmlDump(device.id, "pay_by_Card_Birbank_fail_transactions_" + QString::number(m_account.id)); + } + + emit finishedWithResult(m_error); +} diff --git a/android/birbank/PayByCardBirbankScript.h b/android/birbank/PayByCardBirbankScript.h new file mode 100644 index 0000000..0d73ff3 --- /dev/null +++ b/android/birbank/PayByCardBirbankScript.h @@ -0,0 +1,30 @@ +#pragma once +#include "CommonBirbankScript.h" +#include "db/DeviceInfo.h" + +class PayByCardBirbankScript final : public CommonBirbankScript { + Q_OBJECT + +public: + ~PayByCardBirbankScript() override; + + PayByCardBirbankScript( + AccountInfo account, + QString card, + QString fullName, + double amount, + QObject *parent = nullptr + ); + +protected: + void doStart() override; + +private: + const AccountInfo m_account; + const QString m_card; + const QString m_fullName; + const double m_amount; + QString m_error; + + void makePayment(const DeviceInfo &device); +}; diff --git a/android/ozon/CommonScript.cpp b/android/ozon/CommonScript.cpp new file mode 100644 index 0000000..19d32fd --- /dev/null +++ b/android/ozon/CommonScript.cpp @@ -0,0 +1,281 @@ +#include "CommonScript.h" + +#include +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "net/NetworkService.h" + +namespace Ozon { + +CommonScript::CommonScript(QObject *parent) + : QObject(parent) { +} + +CommonScript::~CommonScript() = default; + +void CommonScript::start() { + doStart(); + emit finished(); +} + +Node CommonScript::swipeToButton( + const QString &deviceId, + const QString &text, int &counter, const int width, const int height +) { + for (int i = 0; i < 10; ++i) { + if (Node button = xmlScreenParser.findButtonNode(text, true); button.x() > 0 && button.y() > 0) { + return button; + } + const int x = width / 2; + const int offset = height / 4; + AdbUtils::makeSwipe(deviceId, x, height - offset, x, offset); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++counter)); + } + return {}; +} + +bool CommonScript::inputPinCode(const QString &deviceId, const QList &pincode) { + for (const Node &node: pincode) { + const bool completePincode = AdbUtils::makeTap(deviceId, node.x(), node.y()); + if (!completePincode) { + AdbUtils::makeTap(deviceId, node.x(), node.y()); + } + QThread::msleep(QRandomGenerator::global()->bounded(100, 901)); + } + return true; +} + +bool CommonScript::runAppAndGoToHomeScreen( + const QString &deviceId, + const QString &packageName, + const QString &pinCode, + const int width, + const int height +) { + // Запустить устройство + if (const QString pid = AdbUtils::tryToGetRunningAppPid(deviceId, packageName); !pid.isEmpty()) { + qDebug() << "Process already running, pid: " << pid; + AdbUtils::getScreenDumpAsXml(deviceId, 1000); + AdbUtils::tryToKillApplication(deviceId, packageName); + } + if (!AdbUtils::tryToStartApplication(deviceId, packageName)) { + AdbUtils::takeScreenshot(deviceId, "run_app_error"); + qWarning() << "Process has not started"; + return false; + } + + // Даем минимальное время на запуск + QThread::msleep(4500); + int counter = 0; + bool findAndInputPincode = false; + + while (true) { + counter++; + + // Если более 10 раз мы пытаемся перейти на домашнюю + if (counter > 10) { + AdbUtils::takeScreenshot(deviceId, "too_many_attempts"); + return false; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + + if (xmlScreenParser.isHomeScreen()) { + return true; + } + + // Проверяем на банеры после пин-кодом + Node closeBannerOrDialogNode = xmlScreenParser.tryToFindBannerOrDialog(width, height); + if (closeBannerOrDialogNode.x() != -1 && closeBannerOrDialogNode.y() != -1) { + AdbUtils::makeTap(deviceId, closeBannerOrDialogNode.x(), closeBannerOrDialogNode.y()); + QThread::msleep(1000); + continue; + } + + // Проверяем на банеры перед пин-кодом + Node closeBannerNode = xmlScreenParser.tryToFindSecurityBanner(); + if (!closeBannerNode.isEmpty()) { + AdbUtils::makeTap(deviceId, closeBannerNode.x(), closeBannerNode.y()); + QThread::msleep(1000); + continue; + } + + + // Проверяем, что это экран ввода пин-кода + if (!findAndInputPincode) { + if (const QList pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); + pincode.size() == 4) { + // чтобы пробудить экран + if (Node textPinCodeNode = xmlScreenParser.findTextNode("Введите ПИН"); !textPinCodeNode.isEmpty()) { + AdbUtils::makeTap(deviceId, textPinCodeNode.x(), textPinCodeNode.y()); + } + inputPinCode(deviceId, pincode); + // После ввода пин-код нужно сделать задержку, пока подгрузка идет, чтобы снова не спарсить + findAndInputPincode = true; + QThread::msleep(3000); + continue; + } + } + + QThread::msleep(1000); + } +} + +bool CommonScript::tryToCloseAllBannersOnHomePage(const QString &deviceId, const int width, const int height) { + Node closeBannerOrDialogNode = xmlScreenParser.tryToFindBannerOrDialog(width, height); + if (closeBannerOrDialogNode.x() != -1 && closeBannerOrDialogNode.y() != -1) { + AdbUtils::makeTap(deviceId, closeBannerOrDialogNode.x(), closeBannerOrDialogNode.y()); + return true; + } + + // TODO тут все другие банеры попробовать закрыть + return false; +} + +bool CommonScript::goToAllProducts( + const QString &deviceId, + const int width, + const int height +) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, m_counter)); + if (tryToCloseAllBannersOnHomePage(deviceId, width, height)) { + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + + // Ozon: домашний экран и есть "Финансы" — счета уже доступны + if (xmlScreenParser.isHomeScreen()) { + return true; + } + + // Если по какой-то причине не на домашнем — пробуем тапнуть по вкладке "Финансы" + for (int i = 0; i < 3; ++i) { + if (Node financeTab = xmlScreenParser.findNodeByContentDesc("Финансы"); !financeTab.isEmpty()) { + AdbUtils::makeTap(deviceId, financeTab.x(), financeTab.y()); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (xmlScreenParser.isHomeScreen()) { + return true; + } + } + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + + qDebug() << "goToAllProducts: not home screen"; + return false; +} + +bool CommonScript::findAndTapOnAccount(const QString &deviceId, const QString &accountNumbers) { + QList > accounts = xmlScreenParser.parseAccountsInfo(); + for (auto &[account, node]: accounts) { + // FIXME будем считать что числа последние везде уникальные + if (accountNumbers == account.lastNumbers) { + qDebug() << "findAndTapOnAccount: " << accountNumbers; + AdbUtils::makeTap(deviceId, node.x(), node.y()); + QThread::msleep(1000); + return true; + } + } + return false; +} + + +void CommonScript::postAccountsData(const QList > &accounts) { + if (accounts.isEmpty()) { + return; + } + + auto *thread = new QThread; + auto *worker = new NetworkService; + + QJsonArray list; + for (const QPair &pair: accounts) { + QJsonObject obj; + AccountInfo account = pair.first; + obj["appName"] = account.appCode; + obj["lastNumbers"] = account.lastNumbers; + obj["amount"] = account.amount; + obj["description"] = account.description; + obj["currency"] = account.currency; + obj["status"] = accountStatusToString(account.status); + + list.append(obj); + } + // FIXME доставить десктоп ид из настроеек + worker->setDeviceId(accounts.first().first.deviceId); + worker->setPayload(QJsonDocument(list).toJson()); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &NetworkService::postAccountsData); + connect(worker, &NetworkService::finished, thread, &QThread::quit); + connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void CommonScript::postNewTransactionData(const AccountInfo &account, const TransactionInfo &transaction) { + auto *thread = new QThread; + auto *worker = new NetworkService; + + QJsonObject obj; + obj["id"] = transaction.id; + obj["amount"] = transaction.amount; + obj["name"] = transaction.name; + obj["fee"] = transaction.fee; + obj["status"] = transactionStatusToString(transaction.status); + obj["type"] = transactionTypeToString(transaction.type); + obj["phone"] = transaction.phone; + obj["description"] = transaction.description; + obj["updatedDescription"] = transaction.updatedDescription; + obj["bankName"] = transaction.bankName; + obj["bankTime"] = transaction.bankTime; + obj["bankTransactionId"] = transaction.bankTrExternalId; + + // FIXME доставить десктоп ид из настроеек + worker->setDeviceId(account.deviceId); + worker->addExtra("lastNumbers", account.lastNumbers); + worker->addExtra("transactionId", transaction.id); + worker->setPayload(QJsonDocument(obj).toJson()); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &NetworkService::insertTransactionData); + connect(worker, &NetworkService::finished, thread, &QThread::quit); + connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void CommonScript::updateTransactionData( + const AccountInfo &account, + const int transactionId, + const TransactionStatus status, + const QString &oldDesc, + const QString &newDesc +) { + auto *thread = new QThread; + auto *worker = new NetworkService; + + QJsonObject obj; + obj["id"] = transactionId; + obj["status"] = transactionStatusToString(status); + obj["description"] = newDesc; + obj["updatedDescription"] = oldDesc; + + // FIXME доставить десктоп ид из настроеек + worker->setDeviceId(account.deviceId); + worker->addExtra("lastNumbers", account.lastNumbers); + worker->setPayload(QJsonDocument(obj).toJson()); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &NetworkService::updateTransactionData); + connect(worker, &NetworkService::finished, thread, &QThread::quit); + connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void CommonScript::stop() { + m_running = false; +} + +} // namespace Ozon \ No newline at end of file diff --git a/android/ozon/CommonScript.h b/android/ozon/CommonScript.h new file mode 100644 index 0000000..75d888f --- /dev/null +++ b/android/ozon/CommonScript.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include "ScreenXmlParser.h" + +namespace Ozon { + +class CommonScript : public QObject { + Q_OBJECT + +public: + explicit CommonScript(QObject *parent = nullptr); + + ~CommonScript() override; + +protected: + virtual void doStart() = 0; + + // static method + static bool inputPinCode(const QString &deviceId, const QList &pincode); + + bool goToAllProducts( + const QString &deviceId, + int width, + int height + ); + + bool findAndTapOnAccount(const QString &deviceId, const QString &accountNumbers); + + void postAccountsData(const QList > &accounts); + + void postNewTransactionData(const AccountInfo &account, const TransactionInfo &transaction); + + void updateTransactionData( + const AccountInfo &account, + int transactionId, + TransactionStatus status, + const QString &oldDesc, + const QString &newDesc + ); + + Node swipeToButton( + const QString &deviceId, + const QString &text, + int &counter, int width, int height + ); + + bool runAppAndGoToHomeScreen( + const QString &deviceId, + const QString &packageName, + const QString &pinCode, + int width, + int height + ); + + bool tryToCloseAllBannersOnHomePage( + const QString &deviceId, int width, int height + ); + + bool m_running = true; + ScreenXmlParser xmlScreenParser; + int m_counter = 0; + +signals: + void finished(); + + void finishedWithResult(const QString &result); + +public slots: + virtual void start() final; + + virtual void stop() final; + +private: + Q_DISABLE_COPY(CommonScript); +}; + +} // namespace Ozon \ No newline at end of file diff --git a/android/ozon/LoginAndCheckAccountsScript.cpp b/android/ozon/LoginAndCheckAccountsScript.cpp new file mode 100644 index 0000000..fc3206d --- /dev/null +++ b/android/ozon/LoginAndCheckAccountsScript.cpp @@ -0,0 +1,69 @@ +#include "LoginAndCheckAccountsScript.h" + +#include "adb/AdbUtils.h" +#include "dao/AccountDAO.h" +#include "dao/ApplicationDAO.h" +#include "dao/DeviceDAO.h" + +namespace Ozon { + +LoginAndCheckAccountsScript::LoginAndCheckAccountsScript( + QString deviceId, + QString appCode, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)) { +} + +LoginAndCheckAccountsScript::~LoginAndCheckAccountsScript() = default; + + +void LoginAndCheckAccountsScript::doStart() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); + const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { + m_error = "Cant open the home screen: " + m_deviceId + " " + m_appCode; + qWarning() << m_error; + if (!ApplicationDAO::updatePinCodeStatus(app.id, "no", "Не прошла проверка [дата]")) { + qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode; + } + + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } else { + if (!ApplicationDAO::updatePinCodeStatus(app.id, "yes", "")) { + qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode; + } + if (goToAllProducts(device.id, device.screenWidth, device.screenHeight)) { + QList > accounts = xmlScreenParser.parseAccountsInfo(); + for (auto &[account, node]: accounts) { + account.deviceId = m_deviceId; + account.appCode = m_appCode; + if (!AccountDAO::upsertAccount(account)) { + m_error = "Not updated: " + account.lastNumbers; + qCritical() << m_error; + } + } + + postAccountsData(accounts); + } else { + qWarning() << "Cant go to all products: " << m_deviceId << " " << m_appCode; + } + } + + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); +} + +} // namespace Ozon \ No newline at end of file diff --git a/android/ozon/LoginAndCheckAccountsScript.h b/android/ozon/LoginAndCheckAccountsScript.h new file mode 100644 index 0000000..52a654c --- /dev/null +++ b/android/ozon/LoginAndCheckAccountsScript.h @@ -0,0 +1,27 @@ +#pragma once +#include "CommonScript.h" + +namespace Ozon { + +class LoginAndCheckAccountsScript final : public CommonScript { + Q_OBJECT + +public: + ~LoginAndCheckAccountsScript() override; + + explicit LoginAndCheckAccountsScript( + QString deviceId, + QString appCode, + QObject *parent = nullptr + ); + +protected: + void doStart() override; + +private: + const QString m_deviceId; + const QString m_appCode; + QString m_error; +}; + +} // namespace Ozon \ No newline at end of file diff --git a/android/ozon/PayByPhoneScript.cpp b/android/ozon/PayByPhoneScript.cpp new file mode 100644 index 0000000..bde0c69 --- /dev/null +++ b/android/ozon/PayByPhoneScript.cpp @@ -0,0 +1,340 @@ +#include "PayByPhoneScript.h" + +#include +#include +#include + +#include "DatabaseManager.h" +#include "adb/AdbUtils.h" +#include "dao/AccountDAO.h" +#include "dao/ApplicationDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/TransactionDAO.h" +#include "db/ApplicationInfo.h" +#include "db/DeviceInfo.h" + +namespace Ozon { + +PayByPhoneScript::PayByPhoneScript( + AccountInfo account, + QString phone, + QString bankName, + const double amount, + QObject *parent +) : CommonScript(parent), + m_account(std::move(account)), m_phone(std::move(phone)), + m_bankName(std::move(bankName)), m_amount(amount) { +} + +PayByPhoneScript::~PayByPhoneScript() = default; + + +void PayByPhoneScript::makePayment( + const QString &deviceId, + const QString &pinCode, + int width, + int height +) { + // Оплата со страницы карты + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + Node payBtn = xmlScreenParser.findButtonNode("Оплатить", false); + if (!payBtn.isEmpty()) { + AdbUtils::makeTap(deviceId, payBtn.x(), payBtn.y()); + QThread::msleep(1000); + } else { + AdbUtils::takeScreenshot(deviceId, "pay_by_phone_pay_btn_not_found"); + m_error = "Кнопка 'Оплатить' не нашлась"; + qDebug() << m_error; + return; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + // TODO - По номеру карты В РСХБ и другие банки + Node payTitle = xmlScreenParser.findTextNode("Платежи и переводы"); + if (!payTitle.isEmpty() && payTitle.y() < 300) { + Node payByPhoneBtn = xmlScreenParser.findButtonNode("По номеру телефона В РСХБ и через", false); + if (!payByPhoneBtn.isEmpty()) { + AdbUtils::makeTap(deviceId, payByPhoneBtn.x(), payByPhoneBtn.y()); + QThread::msleep(1000); + } else { + AdbUtils::takeScreenshot(deviceId, "pay_by_phone_not_found_pay_by_phone"); + m_error = "Не смогли найти пункт 'По номеру телефона В РСХБ и через'"; + qDebug() << m_error; + return; + } + } else { + AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_go_to_payments"); + m_error = "Не смогли перейти на экран 'Платежи и переводы'"; + qDebug() << m_error; + return; + } + + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + Node payByPhoneTitle = xmlScreenParser.findTextNode("Перевод по телефону"); + Node continueBtn = xmlScreenParser.findButtonNode("Продолжить", false); + if (!payByPhoneTitle.isEmpty() && payByPhoneTitle.y() < 300 && continueBtn.isEmpty()) { + Node inputPhone = xmlScreenParser.findFirstEditText(); + AdbUtils::makeTap(deviceId, inputPhone.x(), inputPhone.y()); + QThread::msleep(500); + AdbUtils::inputText(deviceId, m_phone); + QThread::msleep(1000); + } else { + AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_find_pay_by_phone"); + m_error = "Не смогли найти пункт 'Перевод по телефону'"; + qDebug() << m_error; + return; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + QString payBankName = m_bankName == QStringLiteral("Россельхозбанк") ? "Россельхозбанк" : "Другой банк через СБП"; + Node payOtherBankBtn = xmlScreenParser.findButtonNode(payBankName, false); + if (!payOtherBankBtn.isEmpty()) { + AdbUtils::makeTap(deviceId, payOtherBankBtn.x(), payOtherBankBtn.y()); + QThread::msleep(1000); + } else { + AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_tap_on_bank"); + m_error = "Не смогли нажать на первичный выбор банка"; + qDebug() << m_error; + return; + } + + if (m_bankName != QStringLiteral("Россельхозбанк")) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + for (int i = 0; i < 5; ++i) { + Node allBanksTitle = xmlScreenParser.findTextNode("Банки участники"); + if (!allBanksTitle.isEmpty() && allBanksTitle.y() < 300) { + Node inputBankName = xmlScreenParser.findFirstEditText(); + AdbUtils::makeTap(deviceId, inputBankName.x(), inputBankName.y()); + QThread::msleep(500); + AdbUtils::enableAdbIMEKeyboard(deviceId); + AdbUtils::inputAdbIMEText(deviceId, m_bankName); + AdbUtils::disableAdbIMEKeyboard(deviceId); + QThread::msleep(1000); + break; + } else { + qDebug() << "--- allBanksTitle not found, i: " << i; + } + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + for (int i = 0; i < 5; ++i) { + Node bankNameListItem = xmlScreenParser.findTextNode(m_bankName); + if (!bankNameListItem.isEmpty()) { + AdbUtils::makeTap(deviceId, bankNameListItem.x(), bankNameListItem.y()); + QThread::msleep(1000); + break; + } else { + qDebug() << "--- bankNameListItem not found, i: " << i; + } + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + + const bool isRshbBank = m_bankName == QStringLiteral("Россельхозбанк"); + QString payByPhoneScreenTitleText = isRshbBank ? "Перевод клиенту РСХБ" : "Перевод по телефону"; + Node payByPhoneScreenTitle = xmlScreenParser.findTextNode(payByPhoneScreenTitleText); + Node continuePayBtn = xmlScreenParser.findButtonNode("Продолжить", false); + + if (isRshbBank || + !payByPhoneScreenTitle.isEmpty() && payByPhoneScreenTitle.y() < 300 && !continuePayBtn.isEmpty()) { + Node sumBtn = xmlScreenParser.findButtonNode("0", false); + if (isRshbBank || !sumBtn.isEmpty()) { + if (!isRshbBank) { + AdbUtils::makeTap(deviceId, sumBtn.x(), sumBtn.y()); + QThread::msleep(500); + } + AdbUtils::inputText(deviceId, QString::number(m_amount)); + QThread::msleep(500); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + Node readyBtn = xmlScreenParser.findTextNode("Готово"); + if (!readyBtn.isEmpty()) { + AdbUtils::makeTap(deviceId, readyBtn.x(), readyBtn.y()); + QThread::msleep(500); + if (isRshbBank) { + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + continuePayBtn = xmlScreenParser.findButtonNode("Продолжить", false); + } + AdbUtils::makeTap(deviceId, continuePayBtn.x(), continuePayBtn.y()); + QThread::msleep(1000); + } else { + AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_tap_on_ready"); + m_error = "Не смогли нажать 'Готово'"; + qDebug() << m_error; + return; + } + } else { + AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_tap_on_continue"); + m_error = "Не смогли нажать 'Продолжить'"; + qDebug() << m_error; + return; + } + } else { + AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_find_bank_title"); + m_error = "Не смогли найти заголовок: " + payByPhoneScreenTitleText; + qDebug() << m_error; + return; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + bool inputedPinCode = false; + for (int i = 0; i < 5; ++i) { + // TODO кнопка содержит "Перевести 10 ₽" если будет от 1000 то скорее всвего будет "Перевести 1 000 ₽", + // TODO не ясно как форматирование будет, поэтому пока на начало зашьем + Node makePayBtn = xmlScreenParser.findButtonNode("Перевести ", true); + if (!makePayBtn.isEmpty()) { + AdbUtils::makeTap(deviceId, makePayBtn.x(), makePayBtn.y()); + QThread::msleep(500); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (const QList pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите код доступа"); + pincode.size() == 4) { + inputPinCode(deviceId, pincode); + inputedPinCode = true; + // После ввода пин-кода, ждем 3с + QThread::msleep(3000); + } + break; + } else { + qWarning() << "--- makePayBtn not found, i: " << i; + } + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + + if (!isRshbBank && !inputedPinCode) { + AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_input_pin_code"); + m_error = "Не смогли ввести пин-код!'"; + qDebug() << m_error; + return; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + + for (int i = 0; i < 10; ++i) { + Node moreDetailBtn = xmlScreenParser.findButtonNode("Детали операции", false); + if (!moreDetailBtn.isEmpty()) { + AdbUtils::makeTap(deviceId, moreDetailBtn.x(), moreDetailBtn.y()); + QThread::msleep(200); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + + // TODO считываем все данные по переводу + TransactionInfo transaction = xmlScreenParser.parseTransactionInfo(width, height); + if (isRshbBank) { + transaction.bankName = m_bankName; + } + transaction.accountId = m_account.id; + transaction.amount = -m_amount; + transaction.type = TransactionType::Phone; + transaction.id = TransactionDAO::insertTransaction(transaction); + if (transaction.id == -1) { + // FIXME alert!! + qDebug() << "Transaction has not inserted..."; + } + + // ждем 10 секунд смену статуса + for (int k = 0; k < 10; ++k) { + Node paymentCompleted = xmlScreenParser.findTextNode("Исполнен", 0, 0, width, height / 2); + if (!paymentCompleted.isEmpty()) { + transaction.status = TransactionStatus::Complete; + if (!TransactionDAO::updateTransactionStatus(transaction.id, TransactionStatus::Complete)) { + qDebug() << "Not updated: " << transaction.id; + } + // TODO обновляем данные + break; + } + + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + + postNewTransactionData(m_account, transaction); + + AdbUtils::takeScreenshot(deviceId, "pay_by_phone_finish_transactions_" + transaction.id); + AdbUtils::takeXmlDump(deviceId, "pay_by_phone_finish_transactions_" + transaction.id); + break; + } else { + qWarning() << "--- moreDetailBtn not found, i: " << i; + } + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + + Node homeBtn = xmlScreenParser.findButtonNode("Вернуться на главную", false); + AdbUtils::makeTap(deviceId, homeBtn.x(), homeBtn.y()); + QThread::msleep(1000); +} + +void PayByPhoneScript::doStart() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId); + const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found...."; + qDebug() << m_error; + emit finishedWithResult(m_error); + return; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); + if (!xmlScreenParser.isHomeScreen()) { + if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { + m_error = "Не смогли дойти до домашней страницы"; + qDebug() << m_error; + emit finishedWithResult(m_error); + return; + } + } + + if (goToAllProducts(device.id, device.screenWidth, device.screenHeight)) { + QList > accounts = xmlScreenParser.parseAccountsInfo(); + for (auto &[account, node]: accounts) { + account.deviceId = m_account.deviceId; + account.appCode = m_account.appCode; + account.status = account.amount > 0 ? AccountStatus::Active : AccountStatus::Off; + if (!AccountDAO::upsertAccount(account)) { + qDebug() << "Not updated: " << account.lastNumbers; + } + } + + postAccountsData(accounts); + + for (auto &[account, node]: accounts) { + // FIXME будем считать что числа последние везде уникальные + if (m_account.lastNumbers == account.lastNumbers) { + if (account.amount >= m_amount) { + AdbUtils::makeTap(device.id, node.x(), node.y()); + QThread::msleep(1000); + } else { + AdbUtils::takeScreenshot(device.id, "pay_by_phone_not_enough_money_" + QString::number(m_account.id)); + m_error = "Not enough money, id: " + QString::number(m_account.id) + + ", amount: " + QString::number(account.amount) + + ", trx amount: " + QString::number(m_amount); + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + break; + } + } + + makePayment(device.id, app.pinCode, device.screenWidth, device.screenHeight); + // AdbUtils::tryToKillApplication(device.id, app.package); TODO - надо ли это??? + } else { + AdbUtils::takeScreenshot(device.id, "pay_by_phone_cant_open_all_products_" + QString::number(m_account.id)); + AdbUtils::tryToKillApplication(device.id, app.package); + m_error = "Не смогли открыть 'Все продукты'"; + qDebug() << m_error; + } + + emit finishedWithResult(m_error); +} + +} // namespace Ozon \ No newline at end of file diff --git a/android/ozon/PayByPhoneScript.h b/android/ozon/PayByPhoneScript.h new file mode 100644 index 0000000..d8c2113 --- /dev/null +++ b/android/ozon/PayByPhoneScript.h @@ -0,0 +1,38 @@ +#pragma once +#include "CommonScript.h" + +namespace Ozon { + +class PayByPhoneScript final : public CommonScript { + Q_OBJECT + +public: + ~PayByPhoneScript() override; + + PayByPhoneScript( + AccountInfo account, + QString phone, + QString bankName, + double amount, + QObject *parent = nullptr + ); + +protected: + void doStart() override; + +private: + const AccountInfo m_account; + const QString m_phone; + const QString m_bankName; + const double m_amount; + QString m_error; + + void makePayment( + const QString &deviceId, + const QString &pinCode, + int width, + int height + ); +}; + +} // namespace Ozon \ No newline at end of file diff --git a/android/ozon/ScreenXmlParser.cpp b/android/ozon/ScreenXmlParser.cpp new file mode 100644 index 0000000..1acd7de --- /dev/null +++ b/android/ozon/ScreenXmlParser.cpp @@ -0,0 +1,1067 @@ +#include "ScreenXmlParser.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "android/xml/Node.h" +#include "db/AccountInfo.h" +#include "db/TransactionInfo.h" +#include "time/DateUtils.h" + +namespace Ozon { + +ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) { +} + +ScreenXmlParser::~ScreenXmlParser() = default; + +static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])"); +static const QRegularExpression onlyDigitsRegex(R"([^0-9])"); +static const QRegularExpression onlyDigitsAndDotRegex(R"([^0-9.])"); + +static const QRegularExpression rePhone(R"raw((\+7\(?\d{3}\)?[- ]?\d{3}[- ]?\d{2}[- ]?\d{2}))raw"); +static const QRegularExpression reFromBank(R"raw(из (.+?) на)raw"); +static const QRegularExpression reToBank(R"raw(в (.+?) через)raw"); +static const QRegularExpression reSbpId(R"(СБП(?: С2С)? ([A-Z0-9]{30,}))"); +static const QRegularExpression reIdAlt(R"(ID перевода (\d+))"); +static const QRegularExpression reDate(R"((?:Дата операции|дата) (\d{2}\.\d{2}\.\d{4}(?: \d{2}:\d{2}:\d{2})?))"); +static const QRegularExpression reSender(R"(от ([^\(]+))"); +static const QRegularExpression reNameAlt(R"(Перевод ([А-Яа-яЁё\s\*]+) в)"); + +static const QRegularExpression reCardData(R"(цифрами\s+(\d\s\d\s\d\s\d).*?Сумма:\s([\d\s]+)\sруб)"); + +Node ScreenXmlParser::tryToFindBannerOrDialog(const int width, const int height) { + // 1. Ищем push banner - он перекрывает весь экран + Node first = m_nodes[0]; + for (const Node &node: m_nodes) { + // Если это однозначно, что пуш-баннер + if (node.resourceId == "aft-popup-pushNotification-iconClose" && node.className == "android.widget.Button") { + return node; + } + // Если есть кнопка "Позже" + if (node.text.trimmed() == "Позже" && node.className == "android.widget.Button") { + return node; + } + // Если нашли пустую кнпоку + if (node.className == "android.widget.Button" && node.clickable + && node.text.isEmpty() && node.contentDesc.isEmpty()) { + // + if (node.x() > width / 2 && node.y() < width / 2) { + return node; + } + } + } + return {}; +} + +/** + * Смотрим что есть текс "Мы заботимся о вашей безопасности" и + * кнопка "Отложить" + * @return координаты кнопки для закрытия + */ +Node ScreenXmlParser::tryToFindSecurityBanner() { + Node text; + Node closeBtn; + bool hasBiometryScreen = false; + Node notNowNode; + + for (const Node &node: m_nodes) { + if (node.text.trimmed() == "Мы заботимся о вашей безопасности" && node.className == "android.app.Dialog") { + text = node; + } + if (node.text.trimmed() == "Отложить" && node.className == "android.widget.Button") { + closeBtn = node; + } + // Экран биометрии "Быстрый вход" — нажимаем "Не сейчас" + if (node.text.trimmed() == "Быстрый вход") { + hasBiometryScreen = true; + } + if (node.text.trimmed() == "Не сейчас") { + notNowNode = node; + } + } + + if (!text.isEmpty() && !closeBtn.isEmpty()) { + return closeBtn; + } + if (hasBiometryScreen && !notNowNode.isEmpty()) { + return notNowNode; + } + return {}; +} + +Node ScreenXmlParser::tryToFindSelectBottomSheet() { + Node closeBtn; + for (const Node &node: m_nodes) { + if ((node.text.trimmed() == "Отмена" || node.text.trimmed() == "Отложить") && node.className == + "android.widget.Button") { + closeBtn = node; + } + } + // Не нужно проверять все, если хотя бы одна координата есть + if (!closeBtn.isEmpty()) { + return closeBtn; + } + return {}; +} + +Node ScreenXmlParser::findTextNode(const QString &text) { + for (const Node &node: m_nodes) { + if (node.className == "android.widget.TextView" && node.text.trimmed() == text) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::findTextNode(const QString &text, const int x1, const int y1, const int x2, const int y2) { + for (const Node &node: m_nodes) { + if (node.className == "android.widget.TextView" && node.text.trimmed() == text + && node.x() >= x1 && node.x() <= x2 && node.y() >= y1 && node.y() <= y2) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::findFirstEditText() { + for (const Node &node: m_nodes) { + if (node.className == "android.widget.EditText" && node.focusable) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::findButtonNode(const QString &text, const bool contains) { + for (const Node &node: m_nodes) { + if (contains) { + if (node.className == "android.widget.Button" && node.text.contains(text)) { + return node; + } + } else { + if (node.className == "android.widget.Button" && node.text.trimmed() == text) { + return node; + } + } + } + return {}; +} + +Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc) { + for (const Node &node: m_nodes) { + if (node.clickable && node.contentDesc.trimmed() == contentDesc) { + return node; + } + } + return {}; +} + +bool ScreenXmlParser::isHomeScreen() { + // Домашний экран Ozon Банка: одновременно присутствуют кнопки "Перевести" и "Пополнить" + bool hasTransfer = false; + bool hasTopUp = false; + for (const Node &node: m_nodes) { + const QString t = node.text.trimmed(); + if (t == "Перевести") hasTransfer = true; + if (t == "Пополнить") hasTopUp = true; + if (hasTransfer && hasTopUp) return true; + } + return false; +} + +QList ScreenXmlParser::findPinCodeNodes(const QString &pinCode) { + // Ozon PIN кнопки — анонимные FrameLayout без text/content-desc. + // Алгоритм не зависит от разрешения: ищем строки из ровно 3 кликабельных узлов. + + // 1. Собираем все кликабельные узлы с ненулевым размером + QList clickable; + for (const Node &node: m_nodes) { + if (node.clickable && node.width() > 0 && node.height() > 0) { + clickable.append(node); + } + } + if (clickable.isEmpty()) return {}; + + // 2. Находим минимальную высоту — это примерный размер одной кнопки. + // Исключаем полноэкранные элементы (WebView и т.п.) фильтром height <= minH * 3. + int minH = INT_MAX; + for (const Node &n: clickable) minH = std::min(minH, n.height()); + + QList candidates; + for (const Node &n: clickable) { + if (n.height() <= minH * 3) candidates.append(n); + } + + // 3. Сортируем по y + std::sort(candidates.begin(), candidates.end(), [](const Node &a, const Node &b) { + return a.y() < b.y(); + }); + + // 4. Группируем в строки: узлы с разницей y < height первого узла строки — одна строка + QList> rows; + for (const Node &node: candidates) { + bool placed = false; + for (QList &row: rows) { + if (std::abs(node.y() - row.first().y()) < row.first().height()) { + row.append(node); + placed = true; + break; + } + } + if (!placed) rows.append({node}); + } + + // 5. Внутри каждой строки сортируем по x (слева направо) + for (QList &row: rows) { + std::sort(row.begin(), row.end(), [](const Node &a, const Node &b) { + return a.x() < b.x(); + }); + } + + // 6. PIN-сетка — строки ровно с 3 узлами (3 столбца) + // Строки 1-3: цифры 1-9 + // Строка 4: (биометрия) [0] (backspace) → берём средний элемент + QList> gridRows; + for (const QList &row: rows) { + if (row.size() == 3) gridRows.append(row); + } + + QMap digitMap; + const QStringList digits19 = {"1", "2", "3", "4", "5", "6", "7", "8", "9"}; + int idx = 0; + for (int r = 0; r < gridRows.size() - 1 && idx < digits19.size(); ++r) { + for (const Node &n: gridRows[r]) { + if (idx < digits19.size()) digitMap[digits19[idx++]] = n; + } + } + if (!gridRows.isEmpty() && gridRows.last().size() >= 2) { + digitMap["0"] = gridRows.last()[1]; // средний столбец последней строки + } + + QList result; + for (const QChar &c: pinCode) { + if (const QString d = QString(c); digitMap.contains(d)) { + result.append(digitMap[d]); + } + } + return result; +} + +QList ScreenXmlParser::tryToFindPinCode(const QString &pinCode, const QString &title) { + bool isPinScreen = false; + for (const Node &node: m_nodes) { + // Баннер — пин-код не вводить + if (node.text.trimmed() == "Отложить" && node.className == "android.widget.Button") { + return {}; + } + // В Ozon PIN экран определяется по ссылке "Не помню код-пароль" + if (node.text.trimmed() == "Не помню код-пароль") { + isPinScreen = true; + } + // Fallback: другие банки используют title текст + if (!title.isEmpty() && node.text.trimmed() == title) { + isPinScreen = true; + } + } + if (isPinScreen) { + return findPinCodeNodes(pinCode); + } + return {}; +} + +QList ScreenXmlParser::getLast2DaysTransactions() { + QList nodes; + for (const Node &node: m_nodes) { + if (node.className == "android.widget.Button" && node.text.contains("рубл") + && (node.text.contains("Минус") || node.text.contains("Плюс"))) { + nodes.append(node); + } + } + return nodes; +} + +/** +* + */ +void ScreenXmlParser::parseAndSaveXml(const QString &xml) { + QDomDocument doc; + doc.setContent(xml); + m_xml = doc.documentElement(); + m_nodes.clear(); + + QXmlStreamReader reader(xml); + while (!reader.atEnd()) { + reader.readNext(); + if (reader.isStartElement() && reader.name() == "node") { + Node node = Node(); + if (reader.attributes().hasAttribute("bounds")) { + QString bounds = reader.attributes().value("bounds").toString(); + if (QRegularExpressionMatch match = boundsRegex.match(bounds); match.hasMatch()) { + node.setBounds( + match.captured(1).toInt(), + match.captured(2).toInt(), + match.captured(3).toInt(), + match.captured(4).toInt() + ); + } + } + if (reader.attributes().hasAttribute("text")) { + node.text = reader.attributes().value("text").toString(); + } + + if (reader.attributes().hasAttribute("content-desc")) { + node.contentDesc = reader.attributes().value("content-desc").toString(); + } + + if (reader.attributes().hasAttribute("class")) { + node.className = reader.attributes().value("class").toString(); + } + + if (reader.attributes().hasAttribute("clickable")) { + node.clickable = reader.attributes().value("clickable").toString() == "true"; + } + if (reader.attributes().hasAttribute("focusable")) { + node.focusable = reader.attributes().value("focusable").toString() == "true"; + } + if (reader.attributes().hasAttribute("resource-id")) { + node.resourceId = reader.attributes().value("resource-id").toString(); + } + + m_nodes.append(node); + } + } +} + +QString getDayOfWeekAsString(const int day_of_week) { + switch (day_of_week) { + case 1: return "понедельник"; + case 2: return "вторник"; + case 3: return "среда"; + case 4: return "четверг"; + case 5: return "пятница"; + case 6: return "суббота"; + case 7: return "воскресенье"; + default: return ""; + } +} + +QString getMonthAsString(const int month) { + switch (month) { + case 1: return "января"; + case 2: return "февраля"; + case 3: return "марта"; + case 4: return "апреля"; + case 5: return "мая"; + case 6: return "июня"; + case 7: return "июля"; + case 8: return "августа"; + case 9: return "сентября"; + case 10: return "октября"; + case 11: return "ноября"; + case 12: return "декабря"; + default: return ""; + } +} + +QDomElement findSubElement(const QDomElement &elem, const QString &index) { + if (elem.hasChildNodes()) { + QDomNodeList subInfoList = elem.childNodes(); + for (QDomNode subInfoNode: subInfoList) { + if (!subInfoNode.isElement()) continue; + QDomElement subInfo = subInfoNode.toElement(); + if (subInfo.attribute("index") == index) { + return subInfo; + } + } + } + return {}; +} + +QString findSubInfo(const QDomElement &elem, const QString &index) { + QDomElement subInfo = findSubElement(elem, index); + if (!subInfo.isNull()) { + return subInfo.attribute("text"); + } + return ""; +} + +bool hasTimeComponent(const QString &datetimeStr) { + QDateTime dt = QDateTime::fromString(datetimeStr, "dd.MM.yyyy HH:mm:ss"); + if (!dt.isValid()) { + return false; // Строка не соответствует формату даты и времени + } + return dt.time() != QTime(0, 0, 0); // Проверка, что время не равно 00:00:00 +} + +QString transformBankDateTime(const QString &date) { + QDateTime dt; + if (date.contains(",")) { + // 10.05.2025, 15:03 + dt = QDateTime::fromString(date, "dd.MM.yyyy, HH:mm"); + } else { + dt = QDateTime::fromString(date, "dd.MM.yyyy HH:mm:ss"); + } + if (!dt.isValid()) { + dt = QDateTime::fromString(date, "dd.MM.yyyy HH:mm"); + } + if (!dt.isValid()) { + return date; // не распарсили + } + return dt.toString("dd.MM.yyyy HH:mm:ss"); +} + +void parseTransferInfo(const QString &text, TransactionInfo &info) { + if (const auto m = reSender.match(text); m.hasMatch()) { + info.name = m.captured(1).trimmed(); + } else if (const auto m1 = reNameAlt.match(text); m1.hasMatch()) { + info.name = m1.captured(1).trimmed(); + } + + if (const auto m = rePhone.match(text); m.hasMatch()) { + info.phone = m.captured(1).trimmed().remove(onlyDigitsRegex); + } + + if (const auto m = reFromBank.match(text); m.hasMatch()) { + if (const QString bank = m.captured(1).trimmed(); !bank.isEmpty()) { + info.bankName = bank; + } + } else if (const auto m1 = reToBank.match(text); m1.hasMatch()) { + if (const QString bank = m1.captured(1).trimmed(); !bank.isEmpty()) { + info.bankName = bank; + } + } + + if (const auto m = reSbpId.match(text); m.hasMatch()) { + info.bankTrExternalId = m.captured(1); + } else if (const auto m1 = reIdAlt.match(text); m1.hasMatch()) { + info.bankTrExternalId = m1.captured(1); + } + + if (const auto m = reDate.match(text); m.hasMatch()) { + const QString date = m.captured(1); + // если не пустая и дата у нас с 00:00 + // if (hasTimeComponent(date)) { + info.bankTime = transformBankDateTime(date); + // } + } +} + +void traverseNodes(const QDomNode &node) { + if (node.isElement()) { + QDomElement elem = node.toElement(); + + if (node.isElement()) { + QDomElement elem = node.toElement(); + + if (elem.tagName() == "node" && + elem.attribute("class") == "android.widget.Button" && + elem.attribute("text") == "Детали операции") { + // Все что ниже деталей операции + QDomNodeList detailInfo = elem.parentNode().childNodes(); + + + for (int j = 0; j < detailInfo.count(); ++j) { + QDomNode info = detailInfo.at(j); + if (!info.isElement()) continue; + QDomElement infoElement = info.toElement(); + QString index = infoElement.attribute("index"); + switch (index.toInt()) { + case 2: + qDebug() << "Дата операции:" << findSubInfo(infoElement, "1"); + break; + case 5: + qDebug() << "Название:" << findSubInfo(infoElement, "1"); + qDebug() << "-------------------------------------"; + // parseTransfer(findSubInfo(infoElement)); + break; + case 7: + qDebug() << "Комиссия:" << findSubInfo(infoElement, "1"); + break; + default: + qDebug() << "Прочее:" << findSubInfo(infoElement, "1"); + break; + } + + /** + Node: "2" "" + ... Node: "0" "Дата операции" + ... Node: "1" "30.04.2025 17:46:29" + Node: "5" "" + ... Node: "0" "Название" + ... Node: "1" "DBO TRANSFER RSHB INTERNET-BANK" + Node: "7" "" + ... Node: "0" "Комиссия" + ... Node: "1" "50 ₽" + */ + + // if (sibElem.hasChildNodes()) { + // QDomNodeList sibChildren = sibElem.childNodes(); + // for (int k = 0; k < sibChildren.count(); ++k) { + // QDomNode sibChild = sibChildren.at(k); + // if (!sibChild.isElement()) continue; + // QDomElement sibChildElem = sibChild.toElement(); + // qDebug() << "... Node:" + // << sibChildElem.attribute("index") + // << sibChildElem.attribute("text"); + // // << sibChildElem.attribute("class"); + // + // + // if (sibChildElem.hasChildNodes()) { + // QDomNodeList sibChildren2 = sibChildElem.childNodes(); + // for (int k = 0; k < sibChildren2.count(); ++k) { + // QDomNode sibChild2 = sibChildren2.at(k); + // if (!sibChild2.isElement()) continue; + // QDomElement sibChildElem2 = sibChild2.toElement(); + // + // qDebug() << "...... Node:" + // << sibChildElem2.attribute("index") + // << sibChildElem2.attribute("text"); + // // << sibChildElem2.attribute("class"); + // + // if (sibChildElem2.hasChildNodes()) { + // QDomNodeList sibChildren3 = sibChildElem2.childNodes(); + // for (int k = 0; k < sibChildren3.count(); ++k) { + // QDomNode sibChild3 = sibChildren3.at(k); + // if (!sibChild3.isElement()) continue; + // QDomElement sibChildElem3 = sibChild3.toElement(); + // + // qDebug() << "......... Node:" + // << sibChildElem3.attribute("index") + // << sibChildElem3.attribute("text"); + // // << sibChildElem2.attribute("class"); + // } + // } + // } + // } + // } + // } + } + } + } + } + + // Рекурсивно обходим всех детей + QDomNode child = node.firstChild(); + while (!child.isNull()) { + traverseNodes(child); + child = child.nextSibling(); + } +} + +QDomNode findNode(const QDomNode &node, const QString &text) { + if (node.isElement()) { + QString nodeText = node.toElement().attribute("text"); + if (nodeText.toLower().startsWith(text)) { + return node; + } + } + + + QDomNode child = node.firstChild(); + while (!child.isNull()) { + QDomNode result = findNode(child, text); + if (!result.isNull()) { + return result; + } + child = child.nextSibling(); + } + return {}; +} + +float extractAmounts(const QString &text) { + // Разрешаем пробелы внутри чисел, захватываем "рубль", "рубля", "рублей" и т.д. + // QRegularExpression re(R"((Плюс|Минус)\s+([\d\s]+)\s+рубл\w*\s+(\d+)\s+копе\w*)"); + QRegularExpression re( + R"((Плюс|Минус)\s+([\d\s]+)\s+рубл\w*\s+(\d+)\s+копе\w*)", + QRegularExpression::UseUnicodePropertiesOption + ); + + QRegularExpressionMatch match = re.match(text); + if (match.hasMatch()) { + QString signStr = match.captured(1); // "Плюс" или "Минус" + QString rublesStr = match.captured(2).remove(' '); // "1 000" → "1000" + int rubles = rublesStr.toInt(); + int kopecks = match.captured(3).toInt(); + + int totalKopecks = rubles * 100 + kopecks; + if (signStr == "Минус") { + totalKopecks *= -1; + } + + return totalKopecks / 100.0f; + } + return 0; +} + +QString beforeAmount(const QString &text) { + int index = text.indexOf(QRegularExpression("Плюс|Минус")); + if (index != -1) { + return text.left(index).trimmed(); + } + return text; // если не найдено — вернуть всю строку +} + +double parseAmount(const QString &amountAsString) { + QString numberString = amountAsString; + numberString.remove(QRegularExpression(R"([^\dточка])")); + numberString.replace("точка", "."); + double amount = numberString.toDouble(); + + if (amountAsString.startsWith("-")) { + amount *= -1; + } + + return amount; +} + +QDomElement findElementWithListView(const QDomElement &parent) { + QDomElement element = parent.firstChildElement(); + while (!element.isNull()) { + QDomElement child = element.firstChildElement(); + while (!child.isNull()) { + if (child.hasAttribute("class") && child.attribute("class") == "android.widget.ListView") { + return child; + } + child = child.nextSiblingElement(); + } + + QDomElement found = findElementWithListView(element); + if (!found.isNull()) { + return found; + } + element = element.nextSiblingElement(); + } + return {}; +} + +QList ScreenXmlParser::parseTodayAndYesterdayReportHistory() { + QList transactions; + + QDomElement child = findElementWithListView(m_xml); + QDomNodeList listView = findElementWithListView(m_xml).childNodes(); + qDebug() << listView.count(); + if (!listView.isEmpty()) { + QString date; + for (QDomNode node: listView) { + if (node.hasChildNodes()) { + QString text; + if (node.childNodes().count() > 1) { + date = node.firstChildElement().attribute("text"); + text = node.lastChildElement().attribute("text"); + } else if (node.childNodes().count() == 1) { + text = node.firstChildElement().attribute("text"); + } + + TransactionInfo transaction; + if (text.startsWith("Перевод")) { + QRegularExpression re(R"([+-] \d{1,3}(?: \d{3})* точка \d{2} Рубль$)"); + QRegularExpressionMatch match = re.match(text); + if (match.hasMatch()) { + QString amount = match.captured(0); + transaction.amount = parseAmount(amount.trimmed()); + text.chop(amount.length()); + } + transaction.description = text.trimmed(); + parseTransferInfo(text, transaction); + if (transaction.bankTime.isEmpty()) { + transaction.bankTime = transformBankDateTime(date); + } + } else { + // карта + if (text.contains("без учета комиссии")) { + QRegularExpression re(R"( без учета комиссии -\d{1,3}(?: \d{3})* точка \d{2} Рубль$)"); + QRegularExpressionMatch match = re.match(text); + if (match.hasMatch()) { + QString fee = match.captured(0); + text.chop(fee.length()); + transaction.fee = parseAmount(fee.replace("без учета комиссии", "").trimmed()); + } + } + QRegularExpression re(R"([+-] \d{1,3}(?: \d{3})* точка \d{2} Рубль$)"); + QRegularExpressionMatch match = re.match(text); + if (match.hasMatch()) { + QString amount = match.captured(0); + transaction.amount = parseAmount(amount.trimmed()); + text.chop(amount.length()); + } + transaction.description = text.trimmed(); + if (transaction.bankTime.isEmpty()) { + transaction.bankTime = transformBankDateTime(date); + } + } + transactions.append(transaction); + } + } + } else { + qDebug() << "ListView is empty"; + } + return transactions; +} + +QList ScreenXmlParser::parseTodayAndYesterdayHistory() { + QDate today = DateUtils::getMoscowNow().date(); + QDate yesterday = today.addDays(-1); + QDate beforeYesterday = today.addDays(-2); + + + QString todayTitle = QString("сегодня, %1").arg(getDayOfWeekAsString(today.dayOfWeek())); + QString yesterdayTitle = QString("вчера, %1").arg(getDayOfWeekAsString(yesterday.dayOfWeek())); + QString beforeYesterdayTitle = QString("%1 %2, %3").arg( + QString::number(beforeYesterday.day()), + getMonthAsString(beforeYesterday.month()), + getDayOfWeekAsString(beforeYesterday.dayOfWeek()) + ); + + QDomNode todayNode = findNode(m_xml, todayTitle); + QDomNode yesterdayNode = findNode(m_xml, yesterdayTitle); + + QDomNode parentNode; + if (!yesterdayNode.isNull()) { + parentNode = yesterdayNode.parentNode(); + } else if (!todayNode.isNull()) { + parentNode = todayNode.parentNode(); + } + + + QList transactions; + if (!parentNode.isNull()) { + QDate date = {}; + for (QDomNode transactionNode: parentNode.childNodes()) { + QString transactionShortText = transactionNode.toElement().attribute("text"); + + const QString titleLowerCase = transactionShortText.toLower(); + if (titleLowerCase.startsWith(beforeYesterdayTitle)) { + // только сегодня и вчера парсим, позавчера не трогаем + break; + } + if (titleLowerCase.startsWith(todayTitle)) { + date = today; + transactionShortText = transactionShortText.mid(todayTitle.length() + 1); + } + if (titleLowerCase.startsWith(yesterdayTitle)) { + date = yesterday; + transactionShortText = transactionShortText.mid(yesterdayTitle.length() + 1); + } + + if (transactionShortText.contains("ЕСПП")) { + continue; + } + + float amount = extractAmounts(transactionShortText); + QString shortDesc = beforeAmount(transactionShortText); + + TransactionInfo info; + info.amount = amount; + info.bankTime = date.toString("dd.MM.yyyy"); + info.description = shortDesc; + + transactions.append(info); + } + } + return transactions; +} + +void parse2daysHistory(const QDomNode &node) { + QDate today = DateUtils::getMoscowNow().date(); + QDate tomorrow = today.addDays(-1); + QDate after2Days = today.addDays(-2); + + QString todayTitle = QString("сегодня, %1").arg(getDayOfWeekAsString(today.dayOfWeek())); + QDomNode todayNode = findNode(node, todayTitle); + if (todayNode.isNull()) { + } else { + qDebug() << "todayNode: " << todayNode.toElement().attribute("text"); + } + + QString tomorrowTitle = QString("вчера, %1").arg(getDayOfWeekAsString(tomorrow.dayOfWeek())); + QDomNode tomorrowNode = findNode(node, tomorrowTitle); + if (tomorrowNode.isNull()) { + } else { + qDebug() << "tomorrowNode: " << tomorrowNode.toElement().attribute("text"); + } + + // 30 апреля, Среда + QString after2DaysTitle = QString("%1 %2, %3").arg( + QString::number(after2Days.day()), + getMonthAsString(after2Days.month()), + getDayOfWeekAsString(after2Days.dayOfWeek())); + QDomNode after2DaysNode = findNode(node, after2DaysTitle); + if (after2DaysNode.isNull()) { + qDebug() << "No after2DaysNode"; + } else { + qDebug() << "after2DaysNode: " << after2DaysNode.toElement().attribute("text"); + } + + QDomNode parentNode; + if (!todayNode.isNull() || !tomorrowNode.isNull() || !after2DaysNode.isNull()) { + if (!todayNode.isNull()) { + parentNode = todayNode.parentNode(); + } + if (!tomorrowNode.isNull()) { + parentNode = tomorrowNode.parentNode(); + } + if (!after2DaysNode.isNull()) { + parentNode = after2DaysNode.parentNode(); + } + } + + + if (!parentNode.isNull()) { + for (QDomNode child_node: parentNode.childNodes()) { + QString child_text = child_node.toElement().attribute("text"); + if (child_text.toLower().startsWith(todayTitle)) { + qDebug() << "=== Today: " << todayTitle; + qDebug() << "node: " << beforeAmount(child_text.mid(todayTitle.length() + 1)); + qDebug() << "--> sum: " << QString::number(extractAmounts(child_text), 'f', 2); + // qDebug() << ""; + continue; + } + if (child_text.toLower().startsWith(tomorrowTitle)) { + qDebug() << "=== Yesterday: " << todayTitle; + qDebug() << "node: " << beforeAmount(child_text.mid(tomorrowTitle.length() + 1)); + qDebug() << "--> sum: " << QString::number(extractAmounts(child_text), 'f', 2); + // qDebug() << ""; + continue; + } + if (child_text.toLower().startsWith(after2DaysTitle)) { + qDebug() << "=== Before yesterday: " << todayTitle; + qDebug() << "node: " << beforeAmount(child_text.mid(after2DaysTitle.length() + 1)); + qDebug() << "--> sum: " << QString::number(extractAmounts(child_text), 'f', 2); + // qDebug() << ""; + continue; + } + + qDebug() << "node: " << beforeAmount(child_node.toElement().attribute("text")); + qDebug() << "--> sum: " << + QString::number(extractAmounts(child_node.toElement().attribute("text")), 'f', 2); + // qDebug() << ""; + } + } + + + // QString d = today.day() > 9 ? QString::number(today.day()) : "0" + QString::number(today.day()); + // QString m = today.month() > 9 ? QString::number(today.month()) : "0" + QString::number(today.month()); + // QString y = QString::number(today.year()); + // QString w = getDayOfWeekAsString(today.dayOfWeek()); + // QString mm = getMonthAsString(today.month()); + // qDebug() << "Date:" << d << m << y << w << mm; + + + // QString d2 = after2Days.day() > 9 ? QString::number(after2Days.day()) : "0" + QString::number(after2Days.day()); + // QString m2 = after2Days.month() > 9 + // ? QString::number(after2Days.month()) + // : "0" + QString::number(after2Days.month()); + // QString y2 = QString::number(after2Days.year()); + // QString w2 = getDayOfWeekAsString(after2Days.dayOfWeek()); + // QString mm2 = getMonthAsString(after2Days.month()); + // qDebug() << "Date:" << d2 << m2 << y2 << w2 << mm2; +} + +TransactionInfo parseExpandedInfoHistory(const QDomElement &root) { + if (root.isElement()) { + QDomElement elem = root.toElement(); + if (elem.tagName() == "node" + && elem.attribute("class") == "android.widget.Button" + && elem.attribute("text") == "Детали операции" + ) { + TransactionInfo transaction; + // Все что ниже деталей операции + QDomNodeList detailInfo = elem.parentNode().childNodes(); + + for (int i = 0; i < detailInfo.count(); ++i) { + QDomNode info = detailInfo.at(i); + if (!info.isElement()) continue; + + switch (QDomElement e = info.toElement(); e.attribute("index").toInt()) { + case 2: + // Дата операции + transaction.bankTime = transformBankDateTime(findSubInfo(e, "1")); + break; + case 5: + transaction.description = findSubInfo(e, "1"); + parseTransferInfo(transaction.description, transaction); + break; + case 7: + // Комиссия + transaction.fee = findSubInfo(e, "1").remove(onlyDigitsRegex).toFloat(); + break; + default: + // qDebug() << "Прочее:" << findSubInfo(e); + break; + } + } + return transaction; + } + } + + // Рекурсивно обходим всех детей + QDomNode child = root.firstChild(); + while (!child.isNull()) { + if (TransactionInfo info = parseExpandedInfoHistory(child.toElement()); !info.bankTime.isEmpty()) { + return info; + } + child = child.nextSibling(); + } + + return {}; +} + +TransactionInfo ScreenXmlParser::parseTransactionInfoHistory(int screenWidth, int screenHeight) { + // 1. Распарсить описание транзакции + TransactionInfo transaction = parseExpandedInfoHistory(m_xml); + // 1. Найти статус: Исполнен | Операция исполнена + transaction.status = TransactionStatus::Wait; + Node status1 = findTextNode("Исполнен", 0, 0, screenWidth, screenHeight / 2); + Node status2 = findTextNode("Операция исполнена", 0, 0, screenWidth, screenHeight / 2); + if (!status1.isEmpty() || !status2.isEmpty()) { + transaction.status = TransactionStatus::Complete; + } + return transaction; +} + +TransactionInfo parseExpandedInfo(const QDomElement &root) { + if (root.isElement()) { + QDomElement elem = root.toElement(); + if (elem.tagName() == "node" + && elem.attribute("class") == "android.widget.Button" + && elem.attribute("text") == "Детали операции" + ) { + TransactionInfo transaction; + // Все что ниже деталей операции + QDomNodeList detailInfo = elem.parentNode().childNodes(); + + for (int i = 0; i < detailInfo.count(); ++i) { + QDomNode info = detailInfo.at(i); + if (!info.isElement()) continue; + + QDomElement infoElement = info.toElement(); + QString index = infoElement.attribute("index"); + QDomElement ee; + switch (index.toInt()) { + case 3: + transaction.bankTime = transformBankDateTime(findSubInfo(infoElement, "1")); + break; + case 4: + ee = findSubElement(infoElement, "0"); + ee = findSubElement(ee, "1"); + transaction.phone = findSubInfo(ee, "0").remove(onlyDigitsRegex); + break; + case 5: + transaction.name = findSubInfo(infoElement, "1"); + break; + case 6: + transaction.bankName = findSubInfo(infoElement, "1"); + break; + case 9: + transaction.fee = findSubInfo(infoElement, "1").remove(onlyDigitsAndDotRegex).toFloat(); + break; + case 10: + ee = findSubElement(infoElement, "1"); + transaction.bankTrExternalId = findSubInfo(ee, "1"); + break; + default: + break; + } + } + return transaction; + } + } + + // Рекурсивно обходим всех детей + QDomNode child = root.firstChild(); + while (!child.isNull()) { + if (TransactionInfo info = parseExpandedInfo(child.toElement()); !info.bankTime.isEmpty()) { + return info; + } + child = child.nextSibling(); + } + + return {}; +} + +TransactionInfo ScreenXmlParser::parseTransactionInfo(int screenWidth, int screenHeight) { + // 1. Распарсить описание транзакции + TransactionInfo transaction = parseExpandedInfo(m_xml); + // 1. Найти статус: Исполнен | Операция исполнена + transaction.status = TransactionStatus::Wait; + Node status1 = findTextNode("Исполнен", 0, 0, screenWidth, screenHeight / 2); + Node status2 = findTextNode("Операция исполнена", 0, 0, screenWidth, screenHeight / 2); + if (!status1.isEmpty() || !status2.isEmpty()) { + transaction.status = TransactionStatus::Complete; + } + return transaction; +} + + +QList > ScreenXmlParser::parseAccountsInfo() { + QList > accounts; + for (const Node &node: m_nodes) { + const QString text = node.text; + + // Проверяем только нужные строки + if (!text.contains("Номер карты с последними цифрами")) { + continue; + } + QRegularExpressionMatch match = reCardData.match(text); + if (!match.hasMatch()) { + continue; + } + + AccountInfo info; + info.lastNumbers = match.captured(1).remove(" "); + info.amount = match.captured(2).remove(" ").toFloat(); + info.description = text.mid(0, text.indexOf("Номер карты")); + info.updateTime = DateUtils::getUtcNow(); + + accounts.append(qMakePair(info, node)); + } + + return accounts; +} + +void ScreenXmlParser::test() { + // TransactionInfo info2 = parseExpandedInfo(m_xml); + // qDebug().noquote().nospace() << convertTransactionToString(info2); + // return; + + QFile file("assets/rshb/all_products.xml"); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qDebug() << "Cannot open file"; + } + + QDomDocument doc; + if (!doc.setContent(&file)) { + qDebug() << "Failed to parse XML"; + } + file.close(); + + qDebug() << "Start parsing XML:"; + + // Номер карты с последними цифрами + QDomElement root = doc.documentElement(); + parseAndSaveXml(doc.toString()); + // for (const AccountInfo &info: parseAccountsInfo()) { + // qDebug().noquote().nospace() << convertAccountToString(info); + // } + // TransactionInfo info = parseExpandedInfo(root); + // qDebug().noquote().nospace() << convertTransactionToString(info); + // ЕСПП +} + +} // namespace Ozon \ No newline at end of file diff --git a/android/ozon/ScreenXmlParser.h b/android/ozon/ScreenXmlParser.h new file mode 100644 index 0000000..5f520d7 --- /dev/null +++ b/android/ozon/ScreenXmlParser.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include "android/xml/Node.h" +#include "db/AccountInfo.h" +#include "db/TransactionInfo.h" + +namespace Ozon { + +class ScreenXmlParser final : public QObject { + Q_OBJECT + +public: + explicit ScreenXmlParser(QObject *parent = nullptr); + + ~ScreenXmlParser() override; + + Node tryToFindBannerOrDialog(int width, int height); + + Node tryToFindSecurityBanner(); + + Node tryToFindSelectBottomSheet(); + + Node findTextNode(const QString &text); + + Node findTextNode(const QString &text, int x1, int y1, int x2, int y2); + + Node findFirstEditText(); + + Node findButtonNode(const QString &text, bool contains); + + Node findNodeByContentDesc(const QString &contentDesc); + + bool isHomeScreen(); + + + QList tryToFindPinCode(const QString &pinCode, const QString &title); + + QList getLast2DaysTransactions(); + + void parseAndSaveXml(const QString &xml); + + TransactionInfo parseTransactionInfoHistory(int screenWidth, int screenHeight); + TransactionInfo parseTransactionInfo(int screenWidth, int screenHeight); + + QList parseTodayAndYesterdayReportHistory(); + + QList parseTodayAndYesterdayHistory(); + + + QList > parseAccountsInfo(); + + void test(); + +private: + QList m_nodes; + QDomElement m_xml; + + QList findPinCodeNodes(const QString &pinCode); +}; + +} // namespace Ozon \ No newline at end of file diff --git a/android/rshb/CommonScript.cpp b/android/rshb/CommonScript.cpp index 3dda85a..840532e 100644 --- a/android/rshb/CommonScript.cpp +++ b/android/rshb/CommonScript.cpp @@ -8,6 +8,8 @@ #include "adb/AdbUtils.h" #include "net/NetworkService.h" +namespace Rshb { + CommonScript::CommonScript(QObject *parent) : QObject(parent) { } @@ -291,3 +293,5 @@ void CommonScript::updateTransactionData( void CommonScript::stop() { m_running = false; } + +} // namespace Rshb \ No newline at end of file diff --git a/android/rshb/CommonScript.h b/android/rshb/CommonScript.h index 94876fe..5ec8338 100644 --- a/android/rshb/CommonScript.h +++ b/android/rshb/CommonScript.h @@ -3,6 +3,8 @@ #include #include "ScreenXmlParser.h" +namespace Rshb { + class CommonScript : public QObject { Q_OBJECT @@ -72,3 +74,5 @@ public slots: private: Q_DISABLE_COPY(CommonScript); }; + +} // namespace Rshb \ No newline at end of file diff --git a/android/rshb/GetLastDaysHistoryScript.cpp b/android/rshb/GetLastDaysHistoryScript.cpp index 6272d11..e83bfdb 100644 --- a/android/rshb/GetLastDaysHistoryScript.cpp +++ b/android/rshb/GetLastDaysHistoryScript.cpp @@ -15,6 +15,8 @@ #include "net/NetworkService.h" #include "time/DateUtils.h" +namespace Rshb { + GetLastDaysHistoryScript::GetLastDaysHistoryScript( AccountInfo account, QObject *parent @@ -393,3 +395,5 @@ void GetLastDaysHistoryScript::doStart() { AdbUtils::goBack(m_account.deviceId); emit finishedWithResult(m_error); } + +} // namespace Rshb \ No newline at end of file diff --git a/android/rshb/GetLastDaysHistoryScript.h b/android/rshb/GetLastDaysHistoryScript.h index 12cc673..bd49505 100644 --- a/android/rshb/GetLastDaysHistoryScript.h +++ b/android/rshb/GetLastDaysHistoryScript.h @@ -1,6 +1,8 @@ #pragma once #include "CommonScript.h" +namespace Rshb { + class GetLastDaysHistoryScript final : public CommonScript { Q_OBJECT @@ -33,3 +35,5 @@ private: const QString &newDesc ); }; + +} // namespace Rshb \ No newline at end of file diff --git a/android/rshb/HomeScreenScript.cpp b/android/rshb/HomeScreenScript.cpp index 59e9113..9a72cc2 100644 --- a/android/rshb/HomeScreenScript.cpp +++ b/android/rshb/HomeScreenScript.cpp @@ -8,6 +8,8 @@ #include "ScreenXmlParser.h" #include "adb/AdbUtils.h" +namespace Rshb { + HomeScreenScript::HomeScreenScript(QObject *parent) : QObject(parent) { @@ -507,3 +509,5 @@ void HomeScreenScript::start() { void HomeScreenScript::stop() { m_running = false; } + +} // namespace Rshb \ No newline at end of file diff --git a/android/rshb/HomeScreenScript.h b/android/rshb/HomeScreenScript.h index 5e06d42..59b84cb 100644 --- a/android/rshb/HomeScreenScript.h +++ b/android/rshb/HomeScreenScript.h @@ -5,6 +5,8 @@ #include "ScreenXmlParser.h" +namespace Rshb { + class HomeScreenScript final : public QObject { Q_OBJECT @@ -33,3 +35,5 @@ private: int height ); }; + +} // namespace Rshb \ No newline at end of file diff --git a/android/rshb/LoginAndCheckAccountsScript.cpp b/android/rshb/LoginAndCheckAccountsScript.cpp index dfc5b61..9241024 100644 --- a/android/rshb/LoginAndCheckAccountsScript.cpp +++ b/android/rshb/LoginAndCheckAccountsScript.cpp @@ -5,6 +5,8 @@ #include "dao/ApplicationDAO.h" #include "dao/DeviceDAO.h" +namespace Rshb { + LoginAndCheckAccountsScript::LoginAndCheckAccountsScript( QString deviceId, QString appCode, @@ -63,3 +65,5 @@ void LoginAndCheckAccountsScript::doStart() { AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); } + +} // namespace Rshb \ No newline at end of file diff --git a/android/rshb/LoginAndCheckAccountsScript.h b/android/rshb/LoginAndCheckAccountsScript.h index 0a3f676..8d08cfa 100644 --- a/android/rshb/LoginAndCheckAccountsScript.h +++ b/android/rshb/LoginAndCheckAccountsScript.h @@ -1,6 +1,8 @@ #pragma once #include "CommonScript.h" +namespace Rshb { + class LoginAndCheckAccountsScript final : public CommonScript { Q_OBJECT @@ -20,4 +22,6 @@ private: const QString m_deviceId; const QString m_appCode; QString m_error; -}; \ No newline at end of file +}; + +} // namespace Rshb \ No newline at end of file diff --git a/android/rshb/PayByPhoneScript.cpp b/android/rshb/PayByPhoneScript.cpp index 4ed4692..86d6853 100644 --- a/android/rshb/PayByPhoneScript.cpp +++ b/android/rshb/PayByPhoneScript.cpp @@ -13,6 +13,8 @@ #include "db/ApplicationInfo.h" #include "db/DeviceInfo.h" +namespace Rshb { + PayByPhoneScript::PayByPhoneScript( AccountInfo account, QString phone, @@ -334,3 +336,5 @@ void PayByPhoneScript::doStart() { emit finishedWithResult(m_error); } + +} // namespace Rshb \ No newline at end of file diff --git a/android/rshb/PayByPhoneScript.h b/android/rshb/PayByPhoneScript.h index e114725..09b80a4 100644 --- a/android/rshb/PayByPhoneScript.h +++ b/android/rshb/PayByPhoneScript.h @@ -1,6 +1,8 @@ #pragma once #include "CommonScript.h" +namespace Rshb { + class PayByPhoneScript final : public CommonScript { Q_OBJECT @@ -32,3 +34,5 @@ private: int height ); }; + +} // namespace Rshb \ No newline at end of file diff --git a/android/rshb/ScreenXmlParser.cpp b/android/rshb/ScreenXmlParser.cpp index 68566ad..16a3919 100644 --- a/android/rshb/ScreenXmlParser.cpp +++ b/android/rshb/ScreenXmlParser.cpp @@ -15,6 +15,8 @@ #include "db/TransactionInfo.h" #include "time/DateUtils.h" +namespace Rshb { + ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) { } @@ -974,3 +976,5 @@ void ScreenXmlParser::test() { // qDebug().noquote().nospace() << convertTransactionToString(info); // ЕСПП } + +} // namespace Rshb \ No newline at end of file diff --git a/android/rshb/ScreenXmlParser.h b/android/rshb/ScreenXmlParser.h index 11e1fe4..288a566 100644 --- a/android/rshb/ScreenXmlParser.h +++ b/android/rshb/ScreenXmlParser.h @@ -6,6 +6,8 @@ #include "db/AccountInfo.h" #include "db/TransactionInfo.h" +namespace Rshb { + class ScreenXmlParser final : public QObject { Q_OBJECT @@ -55,3 +57,5 @@ private: QList findPinCodeNodes(const QString &pinCode); }; + +} // namespace Rshb \ No newline at end of file diff --git a/android/rshb/SyncAccountDataScript.cpp b/android/rshb/SyncAccountDataScript.cpp index ceb973f..d0078bc 100644 --- a/android/rshb/SyncAccountDataScript.cpp +++ b/android/rshb/SyncAccountDataScript.cpp @@ -1,5 +1,7 @@ #include "SyncAccountDataScript.h" +namespace Rshb { + SyncAccountDataScript::SyncAccountDataScript( QString deviceId, QObject *parent @@ -12,4 +14,6 @@ SyncAccountDataScript::~SyncAccountDataScript() = default; void SyncAccountDataScript::doStart() { // 1. -} \ No newline at end of file +} + +} // namespace Rshb \ No newline at end of file diff --git a/android/rshb/SyncAccountDataScript.h b/android/rshb/SyncAccountDataScript.h index c9da879..a853c5f 100644 --- a/android/rshb/SyncAccountDataScript.h +++ b/android/rshb/SyncAccountDataScript.h @@ -1,6 +1,8 @@ #pragma once #include "CommonScript.h" +namespace Rshb { + class SyncAccountDataScript final : public CommonScript { Q_OBJECT @@ -19,3 +21,4 @@ private: const QString m_deviceId; }; +} // namespace Rshb \ No newline at end of file diff --git a/android/toss/GetLastDaysHistoryTossScript.cpp b/android/toss/GetLastDaysHistoryTossScript.cpp index 323f4be..7499743 100644 --- a/android/toss/GetLastDaysHistoryTossScript.cpp +++ b/android/toss/GetLastDaysHistoryTossScript.cpp @@ -113,7 +113,7 @@ void GetLastDaysHistoryTossScript::doStart() { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); - Node transferText = xmlScreenParser.findTextContainsNode("토스뱅크 통장"); + Node transferText = xmlScreenParser.findFirstAccountLine(); if (!transferText.isEmpty()) { AdbUtils::makeTap(device.id, transferText.x(), transferText.y()); QThread::msleep(3000); @@ -142,11 +142,7 @@ void GetLastDaysHistoryTossScript::doStart() { } } } - } else { - m_error = "Card page not found: " + m_account.deviceId + " " + m_account.lastNumbers; - qWarning() << m_error; - emit finishedWithResult(m_error); - return; + break; } QThread::msleep(1000); diff --git a/android/toss/LoginAndCheckAccountsTossScript.cpp b/android/toss/LoginAndCheckAccountsTossScript.cpp index 23ad96e..ca7feac 100644 --- a/android/toss/LoginAndCheckAccountsTossScript.cpp +++ b/android/toss/LoginAndCheckAccountsTossScript.cpp @@ -1,5 +1,6 @@ #include "LoginAndCheckAccountsTossScript.h" +#include #include #include "adb/AdbUtils.h" @@ -63,50 +64,220 @@ void LoginAndCheckAccountsTossScript::doStart() { if (true) { + + if (true) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + auto accounts = xmlScreenParser.parseAccountsInfo(); + if (!accounts.empty()) { + qDebug() << "--- Finish, amount: " << QString::number(accounts[0].amount); + } + return; + } xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - - // искать Transfer кнопку - - // Node transferText = xmlScreenParser.findTextContainsNode("토스뱅크 통장"); - // if (transferText.isEmpty()) { - // } - - Node transferText = xmlScreenParser.findFirstAccountLine(); + // находим первую кнопку Transfer + Node transferText = xmlScreenParser.findButtonNode("Transfer", false); if (!transferText.isEmpty()) { - AdbUtils::makeTap(device.id, transferText.x(), transferText.y()); - QThread::msleep(3000); + AdbUtils::makeTap(m_deviceId, transferText.x(), transferText.y()); + QThread::msleep(1000); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node enterAmount = xmlScreenParser.findEditText("Enter account number"); + if (!enterAmount.isEmpty()) { + AdbUtils::makeTap(m_deviceId, enterAmount.x(), enterAmount.y()); + QThread::msleep(200); - bool historyFound = false; - for (int i = 0; i < 5; ++i) { - Node topUpBtn = xmlScreenParser.findButtonNode("Top up", false); - Node sendBtn = xmlScreenParser.findButtonNode("Send", false); + AdbUtils::inputText(m_deviceId, "51291082442507"); + QThread::msleep(200); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node inputBank = xmlScreenParser.findNodeByClassName("android.widget.Spinner"); + if (!inputBank.isEmpty()) { + AdbUtils::makeTap(m_deviceId, inputBank.x(), inputBank.y()); + QThread::msleep(500); - if (!topUpBtn.isEmpty() && !sendBtn.isEmpty()) { - // подгрузка истории идет - QThread::msleep(2000); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - QList transactions = xmlScreenParser.parsePositiveTransactions(); - for (auto &transaction: transactions) { - qDebug().noquote().nospace() << convertTransactionToString(transaction); + bool foundBank = false; + for (int i = 0; i < 5; ++i) { + Node bank = xmlScreenParser.findTextNode("Hana"); + if (!bank.isEmpty()) { + AdbUtils::makeTap(m_deviceId, bank.x(), bank.y()); + QThread::msleep(200); + foundBank = true; + break; + } else { + AdbUtils::makeSwipe( + m_deviceId, + device.screenWidth / 2, device.screenHeight / 4 * 3, + device.screenWidth / 2, device.screenHeight / 2 + ); + QThread::msleep(500); + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); } - historyFound = true; - break; + + if (!foundBank) { + m_error = "Bank not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node confirmTransfer = xmlScreenParser.findButtonNode("Confirm", false); + if (!confirmTransfer.isEmpty()) { + AdbUtils::makeTap(m_deviceId, confirmTransfer.x(), confirmTransfer.y()); + QThread::msleep(3000); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + bool enterAmountFound = false; + for (int i = 0; i < 10; ++i) { + Node enterAmountText = xmlScreenParser.findTextNode("Enter amount"); + if (!enterAmountText.isEmpty()) { + // AdbUtils::makeTap(m_deviceId, enterAmountText.x(), enterAmountText.y()); + // QThread::msleep(500); + enterAmountFound = true; + break; + } + + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + if (!enterAmountFound) { + m_error = "Enter amount not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node closeBtn = xmlScreenParser.findButtonNode("확인", false); + if (!closeBtn.isEmpty()) { + AdbUtils::makeTap(m_deviceId, closeBtn.x(), closeBtn.y()); + QThread::msleep(200); + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + for (Node &number: xmlScreenParser.getNodeNumbers(4)) { + AdbUtils::makeTap(m_deviceId, number.x(), number.y()); + QThread::msleep(QRandomGenerator::global()->bounded(200, 901)); + } + + // AdbUtils::inputText(m_deviceId, "2"); + QThread::msleep(1000); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node nextBtn = xmlScreenParser.findButtonNode("Next", false); + if (!nextBtn.isEmpty()) { + AdbUtils::makeTap(m_deviceId, nextBtn.x(), nextBtn.y()); + QThread::msleep(1000); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node sendBtn = xmlScreenParser.findButtonNode("Send", false); + if (!sendBtn.isEmpty()) { + AdbUtils::makeTap(m_deviceId, sendBtn.x(), sendBtn.y()); + QThread::msleep(1000); + + xmlScreenParser.parseAndSaveXml( + AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node sendAgainBtn = xmlScreenParser.findButtonNode("Send", false); + if (!sendAgainBtn.isEmpty()) { + AdbUtils::makeTap(m_deviceId, sendAgainBtn.x(), sendAgainBtn.y()); + QThread::msleep(1000); + } + + bool findAndInputPincode = false; + xmlScreenParser.parseAndSaveXml( + AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + if (const QList pincode = xmlScreenParser.findPinCodeNodes(app.pinCode); + pincode.size() == 6) { + for (const Node &node: pincode) { + AdbUtils::makeTap(m_deviceId, node.x(), node.y()); + QThread::msleep(QRandomGenerator::global()->bounded(200, 901)); + } + findAndInputPincode = true; + QThread::msleep(2000); + } + if (!findAndInputPincode) { + m_error = "Pincode not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + xmlScreenParser.parseAndSaveXml( + AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + bool foundConfirmBtn = false; + for (int i = 0; i < 5; ++i) { + Node confirmBtn = xmlScreenParser.findButtonNode("Confirm", false); + if (!confirmBtn.isEmpty()) { + AdbUtils::makeTap(m_deviceId, confirmBtn.x(), confirmBtn.y()); + foundConfirmBtn = true; + break; + } + + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + if (!foundConfirmBtn) { + m_error = "Last Confirm button not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + for (int i = 0; i < 10; ++i) { + Node lastBtn = xmlScreenParser.findTextNode("닫기"); + Node lastBtn2 = xmlScreenParser.findTextNode("Close"); + if (!lastBtn.isEmpty()) { + AdbUtils::makeTap(m_deviceId, lastBtn.x(), lastBtn.y()); + break; + } + if (!lastBtn2.isEmpty()) { + AdbUtils::makeTap(m_deviceId, lastBtn2.x(), lastBtn2.y()); + break; + } + + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + qDebug() << "--- Finish: " << xmlScreenParser.parseAccountAmount(); + } + } else { + m_error = "Next button not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + } else { + m_error = "Confirm transfer button not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + } else { + m_error = "Input bank not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; } - QThread::msleep(1000); - xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - } - if (!historyFound) { - m_error = "Top up or send button not found: " + m_deviceId + " " + m_appCode; - qWarning() << m_error; + } else { + m_error = "Enter account number not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; emit finishedWithResult(m_error); return; } } else { - m_error = "Card not found: " + m_deviceId + " " + m_appCode; - qWarning() << m_error; + m_error = "Transfer button not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; emit finishedWithResult(m_error); return; } diff --git a/android/toss/PayByCardTossScript.cpp b/android/toss/PayByCardTossScript.cpp new file mode 100644 index 0000000..4fc08ab --- /dev/null +++ b/android/toss/PayByCardTossScript.cpp @@ -0,0 +1,350 @@ +#include "PayByCardTossScript.h" + +#include +#include +#include + +#include "DatabaseManager.h" +#include "adb/AdbUtils.h" +#include "dao/AccountDAO.h" +#include "dao/ApplicationDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/TransactionDAO.h" +#include "db/ApplicationInfo.h" +#include "db/DeviceInfo.h" +#include "time/DateUtils.h" + +PayByCardTossScript::PayByCardTossScript( + AccountInfo account, + QString iban, + QString fullName, + const double amount, + QObject *parent +) : CommonTossScript(parent), + m_account(std::move(account)), + m_fullName(std::move(fullName)), m_amount(amount) { +} + +PayByCardTossScript::~PayByCardTossScript() = default; + + +void PayByCardTossScript::makePayment(const DeviceInfo &device) { + // Оплата со страницы карты + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter)); + Node editIbanEditText = xmlScreenParser.findEditTextByResourceId("com.Toss.Tossmobil:id/edit_iban"); + Node fullNameEditText = xmlScreenParser.findEditTextByResourceId("com.Toss.Tossmobil:id/edit_full_name"); + if (!editIbanEditText.isEmpty() && !fullNameEditText.isEmpty()) { + AdbUtils::makeTap(device.id, editIbanEditText.x(), editIbanEditText.y()); + QThread::msleep(500); + // AdbUtils::inputText(device.id, m_iban); + + AdbUtils::makeTap(device.id, editIbanEditText.x(), editIbanEditText.y()); + QThread::msleep(500); + AdbUtils::inputText(device.id, m_fullName); + + Node nextBtn = xmlScreenParser.findButtonNode("NEXT", false); + AdbUtils::makeTap(device.id, nextBtn.x(), nextBtn.y()); + + bool findSenderAccount = false; + for (int i = 0; i < 5; ++i) { + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter)); + + Node senderAccount = xmlScreenParser.findTextNode("Sender Account"); + if (!senderAccount.isEmpty()) { + // Node inputBankName = xmlScreenParser.findAccountButton(m_account.lastNumbers); + // if (!inputBankName.isEmpty()) { + // AdbUtils::makeTap(device.id, inputBankName.x(), inputBankName.y()); + // QThread::msleep(500); + + + // // FXIME скорее всего придется нажать кнопку NEXT + // findSenderAccount = true; + // } + break; + } + } + if (!findSenderAccount) { + m_error = "Не нашли Sender Account"; + qDebug() << m_error; + return; + } + + bool findAmountDetermination = false; + for (int i = 0; i < 5; ++i) { + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter)); + + Node amountDetermination = xmlScreenParser.findTextNode("Amount Determination"); + if (!amountDetermination.isEmpty()) { + Node integerEditText = xmlScreenParser.findEditTextByResourceId( + "com.Toss.Tossmobil:id/edit_text_integer"); + Node decimalEditText = xmlScreenParser.findEditTextByResourceId( + "com.Toss.Tossmobil:id/edit_text_decimal"); + if (!integerEditText.isEmpty() && !decimalEditText.isEmpty()) { + int integerPart = static_cast(m_amount); + AdbUtils::makeTap(device.id, integerEditText.x(), integerEditText.y()); + QThread::msleep(500); + AdbUtils::inputText(device.id, QString::number(integerPart)); + + int decimalPart = static_cast((m_amount - integerPart) * 100); + AdbUtils::makeTap(device.id, decimalEditText.x(), decimalEditText.y()); + QThread::msleep(500); + AdbUtils::inputText(device.id, QString::number(decimalPart)); + + findAmountDetermination = true; + break; + } else { + m_error = "Не нашли поля ввода для ввода суммы перевода"; + qDebug() << m_error; + return; + } + } + } + if (!findAmountDetermination) { + m_error = "Не нашли Amount Determination"; + qDebug() << m_error; + return; + } + + bool findDetailDefinition = false; + for (int i = 0; i < 5; ++i) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter)); + Node fastTransferBtn = xmlScreenParser.findButtonNode("FAST TRANSFER", false); + if (!fastTransferBtn.isEmpty()) { + AdbUtils::makeTap(device.id, fastTransferBtn.x(), fastTransferBtn.y()); + QThread::msleep(500); + continue; + } + + Node detailDefinitionTitle = xmlScreenParser.findTextNode("Detail Definition"); + Node pleaseChooseBtn = xmlScreenParser.findButtonNode("Please choose", false); + if (!detailDefinitionTitle.isEmpty() && !pleaseChooseBtn.isEmpty()) { + AdbUtils::makeTap(device.id, pleaseChooseBtn.x(), pleaseChooseBtn.y()); + QThread::msleep(500); + + Node searchEditText = xmlScreenParser. + findEditTextByResourceId("com.Toss.Tossmobil:id/input_search"); + if (!searchEditText.isEmpty()) { + AdbUtils::makeTap(device.id, searchEditText.x(), searchEditText.y()); + QThread::msleep(500); + // FIXME "Individual Payment" или "Other Payments" если нет одного, то надо другой + AdbUtils::inputText(device.id, "Other Payments"); + QThread::msleep(1000); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter)); + + Node otherPaymentsText = xmlScreenParser.findTextNode("Other Payments"); + if (!otherPaymentsText.isEmpty()) { + AdbUtils::makeTap(device.id, otherPaymentsText.x(), otherPaymentsText.y()); + QThread::msleep(500); + findDetailDefinition = true; + break; + } + } else { + for (int i = 0; i < 5; ++i) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter)); + Node otherPaymentsText = xmlScreenParser.findTextNode("Other Payments"); + if (otherPaymentsText.isEmpty()) { + AdbUtils::makeSwipe( + m_account.deviceId, + device.screenWidth / 2, device.screenHeight / 4 * 3, + device.screenWidth / 2, device.screenHeight / 4 + ); + } else { + AdbUtils::makeTap(device.id, otherPaymentsText.x(), otherPaymentsText.y()); + QThread::msleep(500); + findDetailDefinition = true; + break; + } + QThread::msleep(500); + } + } + } + } + + if (!findDetailDefinition) { + m_error = "Не нашли Detail Definition"; + qDebug() << m_error; + return; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter)); + Node nextBtn4 = xmlScreenParser.findButtonNode("NEXT", false); + AdbUtils::makeTap(device.id, nextBtn4.x(), nextBtn4.y()); + QThread::msleep(500); + + Node checkInfo = xmlScreenParser.findTextNode( + "Check the details of the transaction you shall realize and approve your transaction."); + if (!checkInfo.isEmpty()) { + for (int i = 0; i < 5; ++i) { + Node confirmBtn = xmlScreenParser.findButtonNode("CONFIRM", false); + if (confirmBtn.isEmpty()) { + AdbUtils::makeSwipe( + m_account.deviceId, + device.screenWidth / 2, device.screenHeight / 4 * 3, + device.screenWidth / 2, device.screenHeight / 4 + ); + } else { + AdbUtils::makeTap(device.id, confirmBtn.x(), confirmBtn.y()); + QThread::msleep(500); + break; + } + + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter)); + } + + + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter)); + Node successText = xmlScreenParser.findTextNode("Your transaction is successfully realized."); + if (!successText.isEmpty()) { + Node receiptBtn = xmlScreenParser.findButtonNode("RECEIPT", false); + if (!receiptBtn.isEmpty()) { + AdbUtils::makeTap(device.id, receiptBtn.x(), receiptBtn.y()); + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter)); + + Node sendBtn = xmlScreenParser.findButtonNode("SEND E-MAIL", false); + if (!sendBtn.isEmpty()) { + AdbUtils::takeXmlDump( + device.id, "pay_by_iban_finish_transactions_" + QString::number(m_account.id) + ); + // Успешная транзакция + // TODO считываем все данные по переводу + TransactionInfo transaction; + transaction.name = m_fullName; + transaction.accountId = m_account.id; + transaction.amount = -m_amount; + transaction.fee = 0; + transaction.status = TransactionStatus::Complete; + transaction.type = TransactionType::Iban; + transaction.updateTime = DateUtils::getUtcNow(); + transaction.completeTime = DateUtils::getUtcNow(); + transaction.timestamp = DateUtils::getUtcNow(); + transaction.id = TransactionDAO::insertTransaction(transaction); + if (transaction.id == -1) { + // FIXME alert!! + qDebug() << "Transaction has not inserted..."; + } + + postNewTransactionData(m_account, transaction); + + Node backBtn = xmlScreenParser.findButtonNode("BACK", false); + if (!backBtn.isEmpty()) { + AdbUtils::makeTap(device.id, backBtn.x(), backBtn.y()); + QThread::msleep(500); + + Node returnBtn = xmlScreenParser.findButtonNode("RETURN TO HOME PAGE", false); + if (!returnBtn.isEmpty()) { + AdbUtils::makeTap(device.id, returnBtn.x(), returnBtn.y()); + } + } + } else { + //FIXME не успешная транзакция + AdbUtils::takeXmlDump( + device.id, "pay_by_iban_fail_transactions_receipt_" + QString::number(m_account.id) + ); + m_error = "Транзакция не успешна"; + qDebug() << m_error; + return; + } + } + } else { + m_error = "Транзакция не успешна"; + qDebug() << m_error; + return; + } + } else { + m_error = "Не нашли Check the details title info"; + qDebug() << m_error; + return; + } + } else { + m_error = "Не нашли поля ввода для IBAN"; + qDebug() << m_error; + return; + } +} + +void PayByCardTossScript::doStart() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId); + const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found."; + qDebug() << m_error; + emit finishedWithResult(m_error); + return; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); + if (!xmlScreenParser.isHomeScreen()) { + if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { + m_error = "Не смогли дойти до домашней страницы"; + qDebug() << m_error; + emit finishedWithResult(m_error); + return; + } + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); + Node allTransactionsNav = xmlScreenParser.findTextNode("Transactions"); + if (!allTransactionsNav.isEmpty()) { + AdbUtils::makeTap(device.id, allTransactionsNav.x(), allTransactionsNav.y()); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); + + // перешли на страницу транзакций + Node allTransactionsTitle = xmlScreenParser.findTextNode( + "Transactions", 0, 0, device.screenWidth, device.screenHeight / 2 + ); + Node moneyTransfersBtn = xmlScreenParser.findTextNode("Money Transfers"); + if (!allTransactionsTitle.isEmpty() && !moneyTransfersBtn.isEmpty()) { + AdbUtils::makeTap(device.id, moneyTransfersBtn.x(), moneyTransfersBtn.y()); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); + + // перешли на страницу переводов + Node moneyTransfersTitle = xmlScreenParser.findTextNode( + "Money Transfers", 0, 0, device.screenWidth, device.screenHeight / 2 + ); + Node transferToOtherAccountBtn = xmlScreenParser.findTextNode("Transfer to Other Account"); + if (!moneyTransfersTitle.isEmpty() && !transferToOtherAccountBtn.isEmpty()) { + AdbUtils::makeTap(device.id, transferToOtherAccountBtn.x(), transferToOtherAccountBtn.y()); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); + + Node transferToOtherAccountTitle = xmlScreenParser.findTextNode( + "Transfer to Other Account", 0, 0, device.screenWidth, device.screenHeight / 2 + ); + Node aNewRecipientBtn = xmlScreenParser.findTextNode("A New Recipient"); + if (!transferToOtherAccountTitle.isEmpty() && !aNewRecipientBtn.isEmpty()) { + AdbUtils::makeTap(device.id, aNewRecipientBtn.x(), aNewRecipientBtn.y()); + QThread::msleep(1000); + + makePayment(device); + } else { + m_error = "Не нашли кнопку 'A New Recipient'"; + qDebug() << m_error; + } + } else { + m_error = "Не нашли кнопку 'Transfer to Other Account'"; + qDebug() << m_error; + } + } else { + m_error = "Не нашли кнопку 'Money Transfers'"; + qDebug() << m_error; + } + } else { + m_error = "Не нашли пункт меню 'Transactions'"; + qDebug() << m_error; + } + + if (!m_error.isEmpty()) { + AdbUtils::takeXmlDump(device.id, "pay_by_iban_fail_transactions_" + QString::number(m_account.id)); + } + + emit finishedWithResult(m_error); +} diff --git a/android/toss/PayByCardTossScript.h b/android/toss/PayByCardTossScript.h new file mode 100644 index 0000000..1c13a71 --- /dev/null +++ b/android/toss/PayByCardTossScript.h @@ -0,0 +1,30 @@ +#pragma once +#include "CommonTossScript.h" +#include "db/DeviceInfo.h" + +class PayByCardTossScript final : public CommonTossScript { + Q_OBJECT + +public: + ~PayByCardTossScript() override; + + PayByCardTossScript( + AccountInfo account, + QString CardToss, + QString fullName, + double amount, + QObject *parent = nullptr + ); + +protected: + void doStart() override; + +private: + const AccountInfo m_account; + const QString m_CardToss; + const QString m_fullName; + const double m_amount; + QString m_error; + + void makePayment(const DeviceInfo &device); +}; diff --git a/android/toss/ScreenXmlTossParser.cpp b/android/toss/ScreenXmlTossParser.cpp index 328a53c..d0d5e4a 100644 --- a/android/toss/ScreenXmlTossParser.cpp +++ b/android/toss/ScreenXmlTossParser.cpp @@ -68,6 +68,33 @@ Node ScreenXmlTossParser::findEditTextByResourceId(const QString &resourceId) { return {}; } + +QList ScreenXmlTossParser::getNodeNumbers(double value) { + // Формат 'f' с precision = 0 — дробная часть отбрасывается + QString str = QString::number(value, 'f', 0); + // Разбиваем на отдельные символы + QList nodes; + for (QChar ch: str) { + QString num = QString(ch); + for (const Node &node: m_nodes) { + if (node.contentDesc == num && node.className == "android.widget.Button") { + nodes.append(node); + } + } + } + + return nodes; +} + +Node ScreenXmlTossParser::findNodeByClassName(const QString &className) { + for (const Node &node: m_nodes) { + if (node.className == className) { + return node; + } + } + return {}; +} + Node ScreenXmlTossParser::findButtonNode(const QString &text, const bool contains) { for (const Node &node: m_nodes) { if (contains) { @@ -137,7 +164,7 @@ Node convertTossDomElementToNode(const QDomElement &element) { node.clickable = element.attribute("clickable") == "true"; } if (element.hasAttribute("focusable")) { - node.focusable =element.attribute("focusable") == "true"; + node.focusable = element.attribute("focusable") == "true"; } if (element.hasAttribute("resource-id")) { node.resourceId = element.attribute("resource-id"); @@ -383,6 +410,70 @@ float extractKoreanWonFromText(const QString &input) { return numberStr.toFloat(); } +double parseKoreanNumber(const QString &input) { + // Карта корейских единиц в числовые множители + static const QMap unitMap = { + {QStringLiteral("조"), 1e12}, + {QStringLiteral("억"), 1e8}, + {QStringLiteral("만"), 1e4} + }; + // Статические регулярки (Clazy: don't create temporaries) + static const QRegularExpression cleanRe(QStringLiteral("[^0-9.,만억조]")); + static const QRegularExpression segmentRe(QStringLiteral(R"(([\d,]+(?:\.\d+)?)(조|억|만))")); + + QString s = input; + s.remove(cleanRe); + + double total = 0.0; + auto it = segmentRe.globalMatch(s); + qsizetype pos = 0; + + while (it.hasNext()) { + auto match = it.next(); + QString numStr = match.captured(1); + QString unitStr = match.captured(2); + + numStr.remove(','); + bool ok = false; + const double num = numStr.toDouble(&ok); + if (ok) + total += num * unitMap.value(unitStr); + + pos = match.capturedEnd(); + } + + // Обработка остатка (обычное число, возможно с дробной частью) + QString rem = s.mid(static_cast(pos)); + rem.remove(','); + if (!rem.isEmpty()) { + bool ok = false; + const double num = rem.toDouble(&ok); + if (ok) total += num; + } + + + return total; +} + +double ScreenXmlTossParser::parseAccountAmount() { + const QDomNodeList nodes = m_xml.elementsByTagName("node"); + + for (int i = 0; i < nodes.count(); ++i) { + const QDomElement el = nodes.at(i).toElement(); + + // Аккаунтов может быть несколько, вот берем первый по списку + if (el.attribute("text") == "Top up" && el.attribute("class") == "android.widget.Button") { + for (QDomNode node: el.parentNode().childNodes()) { + const QDomElement child = node.toElement(); + if (child.attribute("text").contains("만") || child.attribute("text").contains("원")) { + return parseKoreanNumber(child.attribute("text")); + } + } + } + } + return 0; +} + QList ScreenXmlTossParser::parseAccountsInfo() { const QDomNodeList nodes = m_xml.elementsByTagName("node"); QList accounts; @@ -390,34 +481,28 @@ QList ScreenXmlTossParser::parseAccountsInfo() { for (int i = 0; i < nodes.count(); ++i) { const QDomElement el = nodes.at(i).toElement(); - // 토스뱅크 통장 - if (el.attribute("content-desc").contains("토스뱅크 통장") && el.attribute("content-desc").contains("₩")) { - AccountInfo info; - info.amount = extractKoreanWonFromText(el.attribute("content-desc")); - info.updateTime = DateUtils::getUtcNow(); - accounts.append(info); - break; - // auto amount = el.attribute("text").replace("₼", "").trimmed(); - // const QDomElement parent = el.parentNode().parentNode().toElement(); - // // Перебираем потомков - // QDomNode n = parent.firstChild().firstChild().firstChild().firstChild(); - // while (!n.isNull()) { - // const QDomElement e = n.toElement(); - // if (e.attribute("resource-id") == "az.kapitalbank.mbanking:id/tvNumber") { - // const QString account = e.attribute("text"); - // - // AccountInfo info; - // info.lastNumbers = account; - // info.amount = amount.toFloat(); - // info.description = account; - // info.updateTime = DateUtils::getUtcNow(); - // - // accounts.append(info); - // break; - // } - // n = n.nextSibling(); - // } + + // Аккаунтов может быть несколько, вот берем первый по списку + if (el.attribute("text") == "Transfer") { + for (QDomNode node: el.parentNode().parentNode().parentNode().childNodes()) { + const QDomElement child = node.toElement(); + if (child.attribute("content-desc").contains("₩")) { + AccountInfo info; + info.amount = extractKoreanWonFromText(child.attribute("content-desc")); + info.updateTime = DateUtils::getUtcNow(); + accounts.append(info); + return accounts; + } + } } + + // if (el.attribute("content-desc").contains("토스뱅크 통장") && el.attribute("content-desc").contains("₩")) { + // AccountInfo info; + // info.amount = extractKoreanWonFromText(el.attribute("content-desc")); + // info.updateTime = DateUtils::getUtcNow(); + // accounts.append(info); + // break; + // } } return accounts; } diff --git a/android/toss/ScreenXmlTossParser.h b/android/toss/ScreenXmlTossParser.h index b05267a..9a97b7a 100644 --- a/android/toss/ScreenXmlTossParser.h +++ b/android/toss/ScreenXmlTossParser.h @@ -26,6 +26,8 @@ public: Node findEditTextByResourceId(const QString &resourceId); + Node findNodeByClassName(const QString &className); + Node findButtonNode(const QString &text, bool contains); bool isHomeScreen(); @@ -36,8 +38,12 @@ public: QList parseAccountsInfo(); + double parseAccountAmount(); + QList findPinCodeNodes(const QString &pinCode); + QList getNodeNumbers(double value); + void test(); private: diff --git a/assets/ozon/ask_biometry.png b/assets/ozon/ask_biometry.png new file mode 100644 index 0000000..2f7ba83 Binary files /dev/null and b/assets/ozon/ask_biometry.png differ diff --git a/assets/ozon/ask_biometry.xml b/assets/ozon/ask_biometry.xml new file mode 100644 index 0000000..fc8bfed --- /dev/null +++ b/assets/ozon/ask_biometry.xml @@ -0,0 +1,450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/home_ad_1.png b/assets/ozon/home_ad_1.png new file mode 100644 index 0000000..a9ab1f5 Binary files /dev/null and b/assets/ozon/home_ad_1.png differ diff --git a/assets/ozon/home_ad_1.xml b/assets/ozon/home_ad_1.xml new file mode 100644 index 0000000..1d9c436 --- /dev/null +++ b/assets/ozon/home_ad_1.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/home_page.png b/assets/ozon/home_page.png new file mode 100644 index 0000000..a955747 Binary files /dev/null and b/assets/ozon/home_page.png differ diff --git a/assets/ozon/home_page.xml b/assets/ozon/home_page.xml new file mode 100644 index 0000000..f2a7a37 --- /dev/null +++ b/assets/ozon/home_page.xml @@ -0,0 +1,1507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone.png b/assets/ozon/pay/pay_buy_phone.png new file mode 100644 index 0000000..90dacb2 Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone.png differ diff --git a/assets/ozon/pay/pay_buy_phone.xml b/assets/ozon/pay/pay_buy_phone.xml new file mode 100644 index 0000000..3c7a8fc --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone_check_data.png b/assets/ozon/pay/pay_buy_phone_check_data.png new file mode 100644 index 0000000..53b4732 Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone_check_data.png differ diff --git a/assets/ozon/pay/pay_buy_phone_check_data.xml b/assets/ozon/pay/pay_buy_phone_check_data.xml new file mode 100644 index 0000000..418e793 --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone_check_data.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone_default_bank.png b/assets/ozon/pay/pay_buy_phone_default_bank.png new file mode 100644 index 0000000..145f9fd Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone_default_bank.png differ diff --git a/assets/ozon/pay/pay_buy_phone_default_bank.xml b/assets/ozon/pay/pay_buy_phone_default_bank.xml new file mode 100644 index 0000000..0b19d91 --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone_default_bank.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone_enter_sum.png b/assets/ozon/pay/pay_buy_phone_enter_sum.png new file mode 100644 index 0000000..f4df1e0 Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone_enter_sum.png differ diff --git a/assets/ozon/pay/pay_buy_phone_enter_sum.xml b/assets/ozon/pay/pay_buy_phone_enter_sum.xml new file mode 100644 index 0000000..6de42d5 --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone_enter_sum.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone_select_bank.png b/assets/ozon/pay/pay_buy_phone_select_bank.png new file mode 100644 index 0000000..2699522 Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone_select_bank.png differ diff --git a/assets/ozon/pay/pay_buy_phone_select_bank.xml b/assets/ozon/pay/pay_buy_phone_select_bank.xml new file mode 100644 index 0000000..381780d --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone_select_bank.xml @@ -0,0 +1,1290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone_select_bank_1.png b/assets/ozon/pay/pay_buy_phone_select_bank_1.png new file mode 100644 index 0000000..64f7b61 Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone_select_bank_1.png differ diff --git a/assets/ozon/pay/pay_buy_phone_select_bank_1.xml b/assets/ozon/pay/pay_buy_phone_select_bank_1.xml new file mode 100644 index 0000000..3dd8103 --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone_select_bank_1.xml @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone_sent_success.png b/assets/ozon/pay/pay_buy_phone_sent_success.png new file mode 100644 index 0000000..09ec8fd Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone_sent_success.png differ diff --git a/assets/ozon/pay/pay_buy_phone_sent_success.xml b/assets/ozon/pay/pay_buy_phone_sent_success.xml new file mode 100644 index 0000000..06bd5b5 --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone_sent_success.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/payments_page.png b/assets/ozon/payments_page.png new file mode 100644 index 0000000..9b6c113 Binary files /dev/null and b/assets/ozon/payments_page.png differ diff --git a/assets/ozon/payments_page.xml b/assets/ozon/payments_page.xml new file mode 100644 index 0000000..6157f6a --- /dev/null +++ b/assets/ozon/payments_page.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/pin_code.png b/assets/ozon/pin_code.png new file mode 100644 index 0000000..6a34cb4 Binary files /dev/null and b/assets/ozon/pin_code.png differ diff --git a/assets/ozon/pin_code.xml b/assets/ozon/pin_code.xml new file mode 100644 index 0000000..45b2465 --- /dev/null +++ b/assets/ozon/pin_code.xml @@ -0,0 +1,1456 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/toss/confirmation_oper.png b/assets/toss/confirmation_oper.png new file mode 100644 index 0000000..e05d0d4 Binary files /dev/null and b/assets/toss/confirmation_oper.png differ diff --git a/assets/toss/confirmation_oper.xml b/assets/toss/confirmation_oper.xml new file mode 100644 index 0000000..bf71ecc --- /dev/null +++ b/assets/toss/confirmation_oper.xml @@ -0,0 +1,716 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/toss/enter_amount.png b/assets/toss/enter_amount.png new file mode 100644 index 0000000..c1029e6 Binary files /dev/null and b/assets/toss/enter_amount.png differ diff --git a/assets/toss/enter_amount.xml b/assets/toss/enter_amount.xml new file mode 100644 index 0000000..26710ad --- /dev/null +++ b/assets/toss/enter_amount.xml @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/toss/enter_amount_error.png b/assets/toss/enter_amount_error.png new file mode 100644 index 0000000..e21e41f Binary files /dev/null and b/assets/toss/enter_amount_error.png differ diff --git a/assets/toss/enter_amount_error.xml b/assets/toss/enter_amount_error.xml new file mode 100644 index 0000000..e64258c --- /dev/null +++ b/assets/toss/enter_amount_error.xml @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/toss/entered_amount.png b/assets/toss/entered_amount.png new file mode 100644 index 0000000..791a96d Binary files /dev/null and b/assets/toss/entered_amount.png differ diff --git a/assets/toss/entered_amount.xml b/assets/toss/entered_amount.xml new file mode 100644 index 0000000..e59b297 --- /dev/null +++ b/assets/toss/entered_amount.xml @@ -0,0 +1,722 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/toss/inputed_account_Number.png b/assets/toss/inputed_account_Number.png new file mode 100644 index 0000000..0d04157 Binary files /dev/null and b/assets/toss/inputed_account_Number.png differ diff --git a/assets/toss/inputed_account_Number.xml b/assets/toss/inputed_account_Number.xml new file mode 100644 index 0000000..5f79f7a --- /dev/null +++ b/assets/toss/inputed_account_Number.xml @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/toss/last_step.png b/assets/toss/last_step.png new file mode 100644 index 0000000..4c9899d Binary files /dev/null and b/assets/toss/last_step.png differ diff --git a/assets/toss/last_step.xml b/assets/toss/last_step.xml new file mode 100644 index 0000000..23ec943 --- /dev/null +++ b/assets/toss/last_step.xml @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/toss/pop_up_win.png b/assets/toss/pop_up_win.png new file mode 100644 index 0000000..c668895 Binary files /dev/null and b/assets/toss/pop_up_win.png differ diff --git a/assets/toss/pop_up_win.xml b/assets/toss/pop_up_win.xml new file mode 100644 index 0000000..36f455f --- /dev/null +++ b/assets/toss/pop_up_win.xml @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/toss/select_bank.png b/assets/toss/select_bank.png new file mode 100644 index 0000000..e2f3e61 Binary files /dev/null and b/assets/toss/select_bank.png differ diff --git a/assets/toss/select_bank.xml b/assets/toss/select_bank.xml new file mode 100644 index 0000000..9443bb9 --- /dev/null +++ b/assets/toss/select_bank.xml @@ -0,0 +1,814 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/toss/select_recipient.png b/assets/toss/select_recipient.png new file mode 100644 index 0000000..0a8d8ff Binary files /dev/null and b/assets/toss/select_recipient.png differ diff --git a/assets/toss/select_recipient.xml b/assets/toss/select_recipient.xml new file mode 100644 index 0000000..f1abf8b --- /dev/null +++ b/assets/toss/select_recipient.xml @@ -0,0 +1,689 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/toss/selected_acc_bank.png b/assets/toss/selected_acc_bank.png new file mode 100644 index 0000000..e8f32f9 Binary files /dev/null and b/assets/toss/selected_acc_bank.png differ diff --git a/assets/toss/selected_acc_bank.xml b/assets/toss/selected_acc_bank.xml new file mode 100644 index 0000000..bcb2659 --- /dev/null +++ b/assets/toss/selected_acc_bank.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/toss/send_operation.png b/assets/toss/send_operation.png new file mode 100644 index 0000000..a2dff27 Binary files /dev/null and b/assets/toss/send_operation.png differ diff --git a/assets/toss/send_operation.xml b/assets/toss/send_operation.xml new file mode 100644 index 0000000..781e5dd --- /dev/null +++ b/assets/toss/send_operation.xml @@ -0,0 +1,594 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/toss/send_operation_1.png b/assets/toss/send_operation_1.png new file mode 100644 index 0000000..4a3d9dd Binary files /dev/null and b/assets/toss/send_operation_1.png differ diff --git a/assets/toss/send_operation_1.xml b/assets/toss/send_operation_1.xml new file mode 100644 index 0000000..b456ba8 --- /dev/null +++ b/assets/toss/send_operation_1.xml @@ -0,0 +1,685 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/toss/win_after_pass.png b/assets/toss/win_after_pass.png new file mode 100644 index 0000000..89a65f8 Binary files /dev/null and b/assets/toss/win_after_pass.png differ diff --git a/assets/toss/win_after_pass.xml b/assets/toss/win_after_pass.xml new file mode 100644 index 0000000..52fccc1 --- /dev/null +++ b/assets/toss/win_after_pass.xml @@ -0,0 +1,427 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config.ini b/config.ini index 3467a69..3e0d34d 100644 --- a/config.ini +++ b/config.ini @@ -1,29 +1,37 @@ [apps] -list=birbank, toss +list=ozon [birbank] android_package_name=az.kapitalbank.mbanking name=Birbank [common] +access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxODU0MzI0fQ.xLalNxKuPK1QV3D94Je1OJITBV-VfIQJG6iSwPa_mMg +api_key=0eee77b62013253e02e1a69bc6a505bb37757fb7 currencies=USD, KRW, AZN, RUB -token=token1753605586797 +desktop_id=6554b2a2-2885-430e-aae2-0045f82f068e +profile_id=c4552156-d179-4a79-9e91-587038c95cf0 +refresh_token=d22175aa-96ec-405d-8c9f-f6cfce1a0913 +token_expires_at=1771854324 version=0.0.1 [db] -dbPath=/Users/trnsmkot/CLionProjects/ARCDeskProject/ARCDeskProject/database/app_data.db +dbPath=~/CLionProjects/ARCDeskProject/ARCDeskProject/database/app_data.db [net] accountUpdate=http://localhost:9999/api/v1/account/update +apiBase=http://93.183.75.134:8005 appInfoUpdate=http://localhost:9999/api/v1/device/update appStatusUpdate=http://localhost:9999/api/v1/application/update -checkAppStatus=http://localhost:9999/api/v1/application/status eventStatusUpdate=http://localhost:9999/api/v1/event/update paymentsGet=http://localhost:9999/api/v1/payments -registerNewUser=http://localhost:9999/api/v1/application/register transactionInsert=http://localhost:9999/api/v1/transaction/insert transactionUpdate=http://localhost:9999/api/v1/transaction/update +[ozon] +android_package_name=ru.ozon.fintech.finance +name=Ozon + [rshb] android_package_name=ru.rshb.dbo name=Росcельхозбанк diff --git a/database/DatabaseManager.cpp b/database/DatabaseManager.cpp index b5551ec..2d7d138 100644 --- a/database/DatabaseManager.cpp +++ b/database/DatabaseManager.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -29,9 +30,15 @@ QSqlDatabase DatabaseManager::database() { if (!connectionPool.contains(name)) { QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", name); const QSettings settings("config.ini", QSettings::IniFormat); + + QString dbPath = settings.value("db/dbPath").toString(); + if (dbPath.startsWith("~")) { + dbPath.replace(0, 1, QDir::homePath()); + } + // qDebug() << settings.value("db/dbPath").toString(); // db.setDatabaseName(QCoreApplication::applicationDirPath() + "/database/app_data.db"); - db.setDatabaseName(settings.value("db/dbPath").toString()); + db.setDatabaseName(dbPath); if (!db.open()) { qWarning() << "Ошибка открытия базы данных:" << db.lastError().text(); diff --git a/database/app_data.db b/database/app_data.db index 1b771c4..20518d8 100644 Binary files a/database/app_data.db and b/database/app_data.db differ diff --git a/database/dao/ApplicationDAO.cpp b/database/dao/ApplicationDAO.cpp index aca2415..c339ebb 100644 --- a/database/dao/ApplicationDAO.cpp +++ b/database/dao/ApplicationDAO.cpp @@ -157,12 +157,12 @@ ApplicationInfo ApplicationDAO::getApplication(const QString &deviceId, const QS QList ApplicationDAO::getApplicationsByDeviceId(const QString &deviceId) { QSqlQuery query(DatabaseManager::instance().database()); QList list; + query.prepare(R"( SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked FROM applications - WHERE device_id = :device_id + WHERE device_id = :device_id AND install = 'yes' )"); - query.bindValue(":device_id", deviceId); if (!query.exec()) { @@ -182,6 +182,21 @@ bool ApplicationDAO::checkInstalledApps(const QString &deviceId, const QSet DeviceDAO::getAllDevices(const bool online) { QList devices; QSqlQuery query(DatabaseManager::instance().database()); QString sql = R"( - SELECT id, status, name, android, screen_width, screen_height, battery, update_time, image + SELECT id, api_id, status, name, android, screen_width, screen_height, battery, update_time, image FROM devices WHERE image IS NOT NULL )"; @@ -71,7 +78,7 @@ DeviceInfo DeviceDAO::getDeviceById(const QString &deviceId) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, status, name, android, screen_width, screen_height, battery, update_time, image + SELECT id, api_id, status, name, android, screen_width, screen_height, battery, update_time, image FROM devices WHERE id = :deviceId )"); @@ -179,6 +186,19 @@ bool DeviceDAO::markDevicesOfflineExcept(const QStringList &onlineDeviceIds) { return true; } +bool DeviceDAO::updateApiId(const QString &deviceId, const QString &apiId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE devices SET api_id = :api_id WHERE id = :id"); + query.bindValue(":api_id", apiId); + query.bindValue(":id", deviceId); + + if (!query.exec()) { + qWarning() << "Ошибка при обновлении api_id устройства:" << query.lastError().text(); + return false; + } + return true; +} + bool DeviceDAO::updateImage(const QString &deviceId, const QByteArray &imageData) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare("UPDATE devices SET image = :image WHERE id = :id"); diff --git a/database/dao/DeviceDAO.h b/database/dao/DeviceDAO.h index ce60436..069d8ab 100644 --- a/database/dao/DeviceDAO.h +++ b/database/dao/DeviceDAO.h @@ -4,10 +4,14 @@ class DeviceDAO { public: + static void migrate(); + [[nodiscard]] static DeviceInfo getDeviceById(const QString &deviceId); [[nodiscard]] static bool upsertDevice(const DeviceInfo &device); + [[nodiscard]] static bool updateApiId(const QString &deviceId, const QString &apiId); + [[nodiscard]] static bool markDevicesOfflineExcept(const QStringList &onlineDeviceIds); [[nodiscard]] static bool updateImage(const QString &deviceId, const QByteArray &imageData); diff --git a/database/dao/TransactionDAO.h b/database/dao/TransactionDAO.h index e3636f4..3ac2321 100644 --- a/database/dao/TransactionDAO.h +++ b/database/dao/TransactionDAO.h @@ -1,6 +1,5 @@ #pragma once #include "db/TransactionInfo.h" -#include "rshb/GetLastDaysHistoryScript.h" class TransactionDAO { public: diff --git a/images/ozon_icon.png b/images/ozon_icon.png new file mode 100644 index 0000000..363c802 Binary files /dev/null and b/images/ozon_icon.png differ diff --git a/main.cpp b/main.cpp index 3459eea..5051fa5 100644 --- a/main.cpp +++ b/main.cpp @@ -6,6 +6,7 @@ #include "database/DatabaseManager.h" #include "database/dao/CommonDataDAO.h" +#include "database/dao/DeviceDAO.h" #include "services/DeviceScreener.h" #include @@ -16,14 +17,11 @@ #include #include "EventHandler.h" -#include "adb/AdbUtils.h" #include "net/NetworkService.h" -#include "rshb/GetLastDaysHistoryScript.h" -#include "rshb/HomeScreenScript.h" -#include "rshb/PayByPhoneScript.h" #include "widget/loader/ErrorWindow.h" #include "widget/loader/LoaderWindow.h" #include "widget/loader/RegistrationWindow.h" +#include void startDeviceScreener(const QCoreApplication &app) { auto *thread = new QThread(); @@ -80,6 +78,21 @@ void startNetworkService(const QCoreApplication &app) { QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() { networkService->stop(); }); + + // Сигнал эмитируется из фонового потока → Qt::QueuedConnection доставит его в главный поток + QObject::connect(networkService, &NetworkService::tokenRefreshFailed, qApp, []() { + QMessageBox *box = new QMessageBox( + QMessageBox::Critical, + "Ошибка авторизации", + "Не удалось обновить токен авторизации.\n\n" + "Проверьте подключение к интернету.\n" + "Приложение продолжит попытки автоматически.", + QMessageBox::Ok + ); + box->setAttribute(Qt::WA_DeleteOnClose); + box->show(); + }, Qt::QueuedConnection); + paymentThread->start(); } @@ -143,67 +156,57 @@ int main(int argc, char *argv[]) { qInstallMessageHandler(messageHandler); - // 1. Если есть токен, проверяем его - // - ошибка сети или сайта [code -1] - // - протух токен, надо заново [code 0] - // - все ок, запуск программы [code 1] - // 2. Если нет токена, выводим, что нет и надо зарегаться [code 0] + // Миграции БД (добавляем новые колонки если их ещё нет) + DeviceDAO::migrate(); + + // 1. Если есть api_key — логинимся и проверяем/создаём desktop + // - ошибка сети или сервера [code -1] + // - неверный api_key [code 0] — показываем окно ввода + // - всё ок [code 1] — запускаем приложение + // 2. Если нет api_key — показываем окно для ввода const QSettings settings("config.ini", QSettings::IniFormat); - const QString token = settings.value("common/token").toString(); + const QString apiKey = settings.value("common/api_key").toString(); - if (false) { - auto *mainWindow = new MainWindow(); - mainWindow->show(); - } else { - if (token.isEmpty()) { - auto *registrationWindow = new RegistrationWindow(); - registrationWindow->show(); - - QObject::connect( - registrationWindow, &RegistrationWindow::onRegisteredSuccess, registrationWindow, - [registrationWindow, &app]() { - auto *mainWindow = new MainWindow(); - mainWindow->show(); - registrationWindow->close(); - startNetworkService(app); - startDeviceScreener(app); - startEventHandler(app); - }, Qt::QueuedConnection); - } else { - auto *loader = new LoaderWindow(token); - loader->show(); - - QObject::connect(loader, &LoaderWindow::loadingFinished, loader, [loader, &app](const int result) { - if (result == -1) { - auto *errorWindow = new ErrorWindow( - "Нет подключения к интернету или сервер не отвечает.\n\nПожалуйста, попробуйте позже." - ); - errorWindow->show(); - } else if (result == 1) { - auto *mainWindow = new MainWindow(); - mainWindow->show(); - startNetworkService(app); - startDeviceScreener(app); - startEventHandler(app); - } else if (result == 0) { - auto *registrationWindow = new RegistrationWindow(); - registrationWindow->show(); - - QObject::connect( - registrationWindow, &RegistrationWindow::onRegisteredSuccess, registrationWindow, - [registrationWindow, &app]() { - auto *mainWindow = new MainWindow(); - mainWindow->show(); - registrationWindow->close(); - startNetworkService(app); - startDeviceScreener(app); - startEventHandler(app); - }, Qt::QueuedConnection); - } - loader->close(); + auto showRegistration = [&app]() { + auto *registrationWindow = new RegistrationWindow(); + registrationWindow->show(); + QObject::connect( + registrationWindow, &RegistrationWindow::onRegisteredSuccess, registrationWindow, + [registrationWindow, &app]() { + auto *mainWindow = new MainWindow(); + mainWindow->show(); + registrationWindow->close(); + startNetworkService(app); + startDeviceScreener(app); + startEventHandler(app); }, Qt::QueuedConnection); - // loader.show(); - } + }; + + if (apiKey.isEmpty()) { + showRegistration(); + } else { + auto *loader = new LoaderWindow(); + loader->show(); + + QObject::connect(loader, &LoaderWindow::loadingFinished, loader, [loader, &app, showRegistration](const int result) { + if (result == -1) { + auto *errorWindow = new ErrorWindow( + "Нет подключения к интернету или сервер не отвечает.\n\nПожалуйста, попробуйте позже." + ); + errorWindow->show(); + } else if (result == 1) { + auto *mainWindow = new MainWindow(); + mainWindow->show(); + startNetworkService(app); + startDeviceScreener(app); + startEventHandler(app); + } else if (result == 0) { + QSettings s("config.ini", QSettings::IniFormat); + s.remove("common/api_key"); + showRegistration(); + } + loader->close(); + }, Qt::QueuedConnection); } return QApplication::exec(); } diff --git a/migrations.sql b/migrations.sql index aa884ae..7c9ef3c 100644 --- a/migrations.sql +++ b/migrations.sql @@ -1,6 +1,7 @@ CREATE TABLE IF NOT EXISTS devices ( id TEXT PRIMARY KEY, + api_id TEXT, status TEXT, name TEXT, android TEXT, diff --git a/models/android/xml/Node.h b/models/android/xml/Node.h index 7ec7aff..044a3bb 100644 --- a/models/android/xml/Node.h +++ b/models/android/xml/Node.h @@ -21,6 +21,9 @@ public: * const int x = (x1() + x2()) / 2; * const int y = (y1() + y2()) / 2; */ + [[nodiscard]] int width() const { return m_x2 - m_x1; } + [[nodiscard]] int height() const { return m_y2 - m_y1; } + [[nodiscard]] int x() const { if (m_x1 < 0) { return -1; diff --git a/models/db/DeviceInfo.h b/models/db/DeviceInfo.h index d63b5f0..3af8a7b 100644 --- a/models/db/DeviceInfo.h +++ b/models/db/DeviceInfo.h @@ -3,7 +3,8 @@ #include struct DeviceInfo { - QString id; + QString id; // ADB device id (e.g. "emulator-5554") + QString apiId; // UUID from backend API (DeviceResponseModel.id) QString status; QString name; QString android; diff --git a/models/db/EventInfo.h b/models/db/EventInfo.h index 1117ff8..c5f356c 100644 --- a/models/db/EventInfo.h +++ b/models/db/EventInfo.h @@ -16,6 +16,7 @@ enum class EventType { Card, Balance, History, + FetchProfile, Unknown }; @@ -44,6 +45,7 @@ inline QString eventTypeToString(const EventType type) { case EventType::Card: return "card"; case EventType::Balance: return "balance"; case EventType::History: return "history"; + case EventType::FetchProfile: return "fetch_profile"; default: return "unknown"; } } @@ -53,6 +55,7 @@ inline EventType eventTypeFromString(const QString &str) { if (str == "card") return EventType::Card; if (str == "balance") return EventType::Balance; if (str == "history") return EventType::History; + if (str == "fetch_profile") return EventType::FetchProfile; return EventType::Unknown; } diff --git a/services/DeviceScreener.cpp b/services/DeviceScreener.cpp index b2ffd53..f54d68d 100644 --- a/services/DeviceScreener.cpp +++ b/services/DeviceScreener.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,7 @@ DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) { const QSettings settings("config.ini", QSettings::IniFormat); m_urlAppUpdate = settings.value("net/appInfoUpdate").toString(); m_token = settings.value("common/token").toString(); + m_apiBase = settings.value("net/apiBase").toString(); } DeviceScreener::~DeviceScreener() = default; @@ -98,6 +100,88 @@ void DeviceScreener::postDevicesData(const QList &devices) { reply->deleteLater(); } +QJsonValue DeviceScreener::apiPost(const QString &path, const QJsonObject &body) { + if (!m_manager) m_manager = new QNetworkAccessManager(this); + + const QSettings settings("config.ini", QSettings::IniFormat); + QNetworkRequest request(QUrl(m_apiBase + path)); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", + ("Bearer " + settings.value("common/access_token").toString()).toUtf8()); + + QNetworkReply *reply = m_manager->post(request, QJsonDocument(body).toJson()); + QEventLoop loop; + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + loop.exec(); + + QJsonValue result; + if (reply->error() == QNetworkReply::NoError) { + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + if (doc.isObject()) { + const QJsonObject root = doc.object(); + if (root["success"].toBool()) result = root["data"]; + else qCritical() << "DeviceScreener apiPost error:" << root; + } + } else { + qCritical() << "DeviceScreener apiPost network error:" << reply->errorString(); + } + reply->deleteLater(); + return result; +} + +void DeviceScreener::apiPatch(const QString &path, const QJsonObject &body) { + if (!m_manager) m_manager = new QNetworkAccessManager(this); + + const QSettings settings("config.ini", QSettings::IniFormat); + QNetworkRequest request(QUrl(m_apiBase + path)); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", + ("Bearer " + settings.value("common/access_token").toString()).toUtf8()); + + QNetworkReply *reply = m_manager->sendCustomRequest(request, "PATCH", QJsonDocument(body).toJson()); + QEventLoop loop; + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + loop.exec(); + + if (reply->error() != QNetworkReply::NoError) { + qCritical() << "DeviceScreener apiPatch network error:" << reply->errorString(); + } + reply->deleteLater(); +} + +QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) { + const QSettings settings("config.ini", QSettings::IniFormat); + const QString desktopId = settings.value("common/desktop_id").toString(); + if (desktopId.isEmpty()) { + qWarning() << "createDeviceOnApi: desktop_id не задан"; + return {}; + } + + QJsonObject body; + body["desktop_id"] = desktopId; + body["status"] = "ONLINE"; + body["name"] = device.name; + body["screen_width"] = device.screenWidth; + body["screen_height"] = device.screenHeight; + body["battery"] = device.battery; + + const QJsonValue result = apiPost("/api/v1/device/", body); + if (!result.isNull() && !result.isUndefined()) { + return result.toObject()["id"].toString(); + } + return {}; +} + +void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) { + QJsonObject body; + body["status"] = (device.status == "device") ? "ONLINE" : "OFFLINE"; + body["battery"] = device.battery; + body["update_time"] = QDateTime::currentSecsSinceEpoch(); + apiPatch("/api/v1/device/" + device.apiId, body); +} + void DeviceScreener::start() { QMap bankChecked; while (m_running) { @@ -124,10 +208,29 @@ void DeviceScreener::start() { if (!DeviceDAO::upsertDevice(device)) { qDebug() << "Cant update devive"; } + + // Синхронизация с API + DeviceInfo saved = DeviceDAO::getDeviceById(device.id); + if (saved.apiId.isEmpty()) { + // Устройство ещё не зарегистрировано — создаём + const QString apiId = createDeviceOnApi(device); + if (!apiId.isEmpty()) { + DeviceDAO::updateApiId(device.id, apiId); + qDebug() << "Device registered on API:" << device.id << "->" << apiId; + } + } else if (status == "device") { + // Обновляем не чаще раза в 60 секунд + const qint64 now = QDateTime::currentSecsSinceEpoch(); + if (now - m_lastApiUpdate.value(device.id, 0) >= 60) { + device.apiId = saved.apiId; + updateDeviceOnApi(device); + m_lastApiUpdate[device.id] = now; + } + } } // if (!deviceInfos.empty()) { - postDevicesData(deviceInfos); + // postDevicesData(deviceInfos); // } if (!DeviceDAO::markDevicesOfflineExcept(onlineIds)) { diff --git a/services/DeviceScreener.h b/services/DeviceScreener.h index 07f3961..d42f84c 100644 --- a/services/DeviceScreener.h +++ b/services/DeviceScreener.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include @@ -23,12 +24,21 @@ private: bool m_running = true; QString m_urlAppUpdate; QString m_token; + QString m_apiBase; + QNetworkAccessManager *m_manager = nullptr; + QMap m_lastApiUpdate; // deviceId → unix timestamp последнего PATCH static QList > readAdbDevices(); static QByteArray takeScreenshot(const QString &deviceId); - static DeviceInfo readDeviceInfo(const QString &deviceId, bool isChecked); + static DeviceInfo readDeviceInfo(const QString &deviceId, bool isChecked); void postDevicesData(const QList &devices); + + QJsonValue apiPost(const QString &path, const QJsonObject &body); + void apiPatch(const QString &path, const QJsonObject &body); + + QString createDeviceOnApi(const DeviceInfo &device); + void updateDeviceOnApi(const DeviceInfo &device); }; diff --git a/services/EventHandler.cpp b/services/EventHandler.cpp index cef1fd5..9a3f76a 100644 --- a/services/EventHandler.cpp +++ b/services/EventHandler.cpp @@ -12,6 +12,7 @@ #include "db/ApplicationInfo.h" #include "net/NetworkService.h" #include "birbank/GetLastDaysHistoryBirbankScript.h" +#include "ozon/LoginAndCheckAccountsScript.h" #include "rshb/CommonScript.h" #include "rshb/GetLastDaysHistoryScript.h" #include "rshb/LoginAndCheckAccountsScript.h" @@ -140,31 +141,49 @@ void EventHandler::startThreadForKey(const QString &key) { } ); } - // auto *worker = new GetLastDaysHistoryScript(account); + // auto *worker = new Rshb::GetLastDaysHistoryScript(account); // worker->moveToThread(thr); // - // connect(thr, &QThread::started, worker, &GetLastDaysHistoryScript::start); - // connect(worker, &GetLastDaysHistoryScript::finished, thr, &QThread::quit); - // connect(worker, &GetLastDaysHistoryScript::finished, worker, &QObject::deleteLater); + // connect(thr, &QThread::started, worker, &Rshb::GetLastDaysHistoryScript::start); + // connect(worker, &Rshb::GetLastDaysHistoryScript::finished, thr, &QThread::quit); + // connect(worker, &Rshb::GetLastDaysHistoryScript::finished, worker, &QObject::deleteLater); // connect(thr, &QThread::finished, thr, &QObject::deleteLater); // - // connect(worker, &GetLastDaysHistoryScript::finishedWithResult, this, + // connect(worker, &Rshb::GetLastDaysHistoryScript::finishedWithResult, this, // [this, key, event](const QString &result) { // updateEventThread(key, event, result); // } // ); } + if (event.type == EventType::FetchProfile) { + if ("ozon" == account.appCode) { + auto *worker = new Ozon::LoginAndCheckAccountsScript(event.deviceId, account.appCode); + worker->moveToThread(thr); + + connect(thr, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, thr, &QThread::quit); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); + connect(thr, &QThread::finished, thr, &QObject::deleteLater); + + connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, this, + [this, key, event](const QString &result) { + updateEventThread(key, event, result); + } + ); + } + } + if (event.type == EventType::Phone) { - // auto *worker = new PayByPhoneScript(account, event.phone, event.bankName, event.amount); + // auto *worker = new Rshb::PayByPhoneScript(account, event.phone, event.bankName, event.amount); // worker->moveToThread(thr); // - // connect(thr, &QThread::started, worker, &PayByPhoneScript::start); - // connect(worker, &PayByPhoneScript::finished, thr, &QThread::quit); - // connect(worker, &PayByPhoneScript::finished, worker, &QObject::deleteLater); + // connect(thr, &QThread::started, worker, &Rshb::PayByPhoneScript::start); + // connect(worker, &Rshb::PayByPhoneScript::finished, thr, &QThread::quit); + // connect(worker, &Rshb::PayByPhoneScript::finished, worker, &QObject::deleteLater); // connect(thr, &QThread::finished, thr, &QObject::deleteLater); // - // connect(worker, &PayByPhoneScript::finishedWithResult, this, + // connect(worker, &Rshb::PayByPhoneScript::finishedWithResult, this, // [this, key, event](const QString &result) { // updateEventThread(key, event, result); // } diff --git a/services/net/NetworkService.cpp b/services/net/NetworkService.cpp index 89e0227..55772c0 100644 --- a/services/net/NetworkService.cpp +++ b/services/net/NetworkService.cpp @@ -11,7 +11,6 @@ #include #include "dao/AccountDAO.h" -#include "dao/ApplicationDAO.h" #include "dao/DeviceDAO.h" #include "dao/EventDAO.h" #include "dao/TransactionDAO.h" @@ -25,10 +24,10 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) { m_urlAppStatusUpdate = settings.value("net/appStatusUpdate").toString(); m_urlTransactionInsert = settings.value("net/transactionInsert").toString(); m_urlEventStatusUpdate = settings.value("net/eventStatusUpdate").toString(); - m_urlCheckAppStatus = settings.value("net/checkAppStatus").toString(); - m_urlRegisterNewUser = settings.value("net/registerNewUser").toString(); + m_apiBase = settings.value("net/apiBase").toString(); m_manager = new QNetworkAccessManager(this); m_token = settings.value("common/token").toString(); + m_accessToken = settings.value("common/access_token").toString(); } NetworkService::~NetworkService() = default; @@ -129,16 +128,7 @@ QJsonArray NetworkService::get(const QString &path, const QMap } void NetworkService::postAccountsData() { - QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - - addFormMetadata(multiPart, m_json); - addFormField(multiPart, "token", m_token); - addFormField(multiPart, "androidUniqueName", m_deviceId); - const bool isSuccess = post(multiPart, m_urlAccountUpdate).toBool(); - if (!isSuccess) { - qCritical() << "Cant post accounts data"; - } - + // TODO: заменить на новый API emit finished(); } @@ -191,34 +181,98 @@ void NetworkService::postAppStatus() { emit finished(); } -void NetworkService::checkAppStatus() { - QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - - addFormField(multiPart, "token", m_extraData["token"].toString()); - - const QJsonValue result = post(multiPart, m_urlCheckAppStatus); - if (result.isNull()) { - emit finishedWithResult(-1); - } else { - emit finishedWithResult(result.toInt()); +QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body, bool withAuth) { + const QUrl url(path); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + if (withAuth && !m_accessToken.isEmpty()) { + request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); } - emit finished(); + + QNetworkReply *reply = m_manager->post(request, QJsonDocument(body).toJson()); + QEventLoop loop; + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { + loop.quit(); + }); + loop.exec(); + + QJsonValue result; + if (reply->error() != QNetworkReply::NoError) { + qCritical() << "Network error:" << reply->errorString() << path; + result = QJsonValue::Undefined; + } else { + const QByteArray raw = reply->readAll(); + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); + if (err.error == QJsonParseError::NoError && doc.isObject()) { + const QJsonObject root = doc.object(); + if (root["success"].toBool()) { + result = root["data"]; + } else { + qCritical() << "API error:" << root; + result = QJsonValue::Null; + } + } + } + reply->deleteLater(); + return result; } -void NetworkService::registerNewUser() { - QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); +void NetworkService::loginAndSetup() { QSettings settings("config.ini", QSettings::IniFormat); - addFormField(multiPart, "version", settings.value("common/version").toString()); - addFormField(multiPart, "name", m_extraData["name"].toString()); - addFormField(multiPart, "email", m_extraData["email"].toString()); + const QString apiKey = settings.value("common/api_key").toString(); - const QJsonValue result = post(multiPart, m_urlRegisterNewUser); - if (result.isNull()) { - emit finishedWithResult(-1); - } else { - settings.setValue("common/token", result.toString()); - emit finishedWithResult(1); + // Шаг 1: логин + QJsonObject loginBody; + loginBody["api_key"] = apiKey; + const QJsonValue loginResult = postJson(m_apiBase + "/api/v1/auth/login", loginBody); + + if (loginResult.isUndefined()) { + emit finishedWithResult(-1); // ошибка сети + emit finished(); + return; } + if (loginResult.isNull()) { + emit finishedWithResult(0); // неверный api_key + emit finished(); + return; + } + + const QJsonObject loginData = loginResult.toObject(); + m_accessToken = loginData["access_token"].toString(); + settings.setValue("common/access_token", m_accessToken); + settings.setValue("common/refresh_token", loginData["refresh_token"].toString()); + const qint64 expiresIn = static_cast(loginData["expires_in"].toDouble()); + settings.setValue("common/token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); + + // Шаг 2: проверяем есть ли desktop_id + const QString desktopId = settings.value("common/desktop_id").toString(); + if (!desktopId.isEmpty()) { + emit finishedWithResult(1); + emit finished(); + return; + } + + // Шаг 3: создаём desktop + QJsonObject desktopBody; + desktopBody["blue_token"] = QJsonValue::Null; + desktopBody["name"] = QJsonValue::Null; + desktopBody["contact"] = QJsonValue::Null; + desktopBody["app_version"] = settings.value("common/version").toString(); + const QJsonValue desktopResult = postJson(m_apiBase + "/api/v1/desktop/", desktopBody, true); + + if (desktopResult.isUndefined() || desktopResult.isNull()) { + emit finishedWithResult(-1); + emit finished(); + return; + } + + const QJsonObject desktopData = desktopResult.toObject(); + settings.setValue("common/desktop_id", desktopData["id"].toString()); + settings.setValue("common/profile_id", desktopData["profile_id"].toString()); + + emit finishedWithResult(1); emit finished(); } @@ -270,46 +324,162 @@ void NetworkService::insertTransactionData() { emit finished(); } -void NetworkService::start() { - QList devices = DeviceDAO::getAllDevices(true); - for (DeviceInfo &device: devices) { - QList applications = ApplicationDAO::getApplicationsByDeviceId(device.id); - for (ApplicationInfo &app: applications) { - if (app.pinCodeChecked) { - QList accounts = AccountDAO::getAccounts(device.id, app.code); - QJsonArray list; +QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &body, bool withAuth) { + const QUrl url(path); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + if (withAuth && !m_accessToken.isEmpty()) { + request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); + } - for (const AccountInfo &account: accounts) { - QJsonObject obj; - obj["appName"] = account.appCode; - obj["lastNumbers"] = account.lastNumbers; - obj["amount"] = account.amount; - obj["description"] = account.description; - obj["currency"] = account.currency; - obj["status"] = accountStatusToString(account.status); - list.append(obj); - } + QNetworkReply *reply = m_manager->sendCustomRequest(request, "PATCH", QJsonDocument(body).toJson()); + QEventLoop loop; + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + loop.exec(); - auto *paymentThread = new QThread; - auto *networkService = new NetworkService; - networkService->setDeviceId(device.id); - networkService->setPayload(QJsonDocument(list).toJson()); - networkService->moveToThread(paymentThread); - QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::postAccountsData); - QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit); - QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); - QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater); - paymentThread->start(); + QJsonValue result; + if (reply->error() != QNetworkReply::NoError) { + qCritical() << "Network error (PATCH):" << reply->errorString() << path; + } else { + const QByteArray raw = reply->readAll(); + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); + if (err.error == QJsonParseError::NoError && doc.isObject()) { + const QJsonObject root = doc.object(); + if (root["success"].toBool()) { + result = root["data"]; + } else { + qCritical() << "API error (PATCH):" << root; } } } + reply->deleteLater(); + return result; +} - QTimer *poller = new QTimer(this); - poller->setInterval(1000); - connect(poller, &QTimer::timeout, this, &NetworkService::getPayments); - poller->start(); +void NetworkService::createDevice() { + const QSettings settings("config.ini", QSettings::IniFormat); + const QString desktopId = settings.value("common/desktop_id").toString(); + if (desktopId.isEmpty()) { + qWarning() << "createDevice: desktop_id not set"; + emit finished(); + return; + } + + const QJsonObject body = QJsonDocument::fromJson(m_json).object(); + const QJsonValue result = postJson(m_apiBase + "/api/v1/device/", body, true); + + if (!result.isNull() && !result.isUndefined()) { + const QString apiId = result.toObject()["id"].toString(); + if (!apiId.isEmpty() && !m_deviceId.isEmpty()) { + DeviceDAO::updateApiId(m_deviceId, apiId); + qDebug() << "createDevice: saved api_id" << apiId << "for" << m_deviceId; + } + } + + emit finished(); +} + +void NetworkService::updateDevice() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); + if (device.apiId.isEmpty()) { + qWarning() << "updateDevice: api_id not set for" << m_deviceId; + emit finished(); + return; + } + + const QJsonObject body = QJsonDocument::fromJson(m_json).object(); + patchJson(m_apiBase + "/api/v1/device/" + device.apiId, body, true); + + emit finished(); +} + +bool NetworkService::refreshToken() { + QSettings settings("config.ini", QSettings::IniFormat); + const QString storedRefreshToken = settings.value("common/refresh_token").toString(); + if (storedRefreshToken.isEmpty()) { + qWarning() << "refreshToken: refresh_token отсутствует"; + return false; + } + + QJsonObject body; + body["refresh_token"] = storedRefreshToken; + const QJsonValue result = postJson(m_apiBase + "/api/v1/auth/refresh_token", body); + + if (result.isNull() || result.isUndefined()) { + qCritical() << "refreshToken: не удалось обновить токен"; + if (!m_tokenErrorShown) { + m_tokenErrorShown = true; + emit tokenRefreshFailed(); + } + return false; + } + + const QJsonObject data = result.toObject(); + m_accessToken = data["access_token"].toString(); + settings.setValue("common/access_token", m_accessToken); + settings.setValue("common/refresh_token", data["refresh_token"].toString()); + const qint64 expiresIn = static_cast(data["expires_in"].toDouble()); + settings.setValue("common/token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); + + m_tokenErrorShown = false; // сбрасываем если снова заработало + qDebug() << "refreshToken: токен обновлён, истекает через" << expiresIn << "сек"; + return true; +} + +bool NetworkService::ensureValidToken() { + const QSettings settings("config.ini", QSettings::IniFormat); + const qint64 expiresAt = settings.value("common/token_expires_at", 0).toLongLong(); + const qint64 now = QDateTime::currentSecsSinceEpoch(); + + // Обновляем заранее — за 30 минут до истечения + if (expiresAt - now < 30 * 60) { + qDebug() << "ensureValidToken: токен истекает через" << (expiresAt - now) << "сек, обновляем"; + return refreshToken(); + } + return true; +} + +void NetworkService::scheduleNextRefresh() { + if (!m_running) return; + + const QSettings settings("config.ini", QSettings::IniFormat); + const qint64 expiresAt = settings.value("common/token_expires_at", 0).toLongLong(); + const qint64 now = QDateTime::currentSecsSinceEpoch(); + + // Обновляем за 30 минут до истечения; если уже пора — сразу + const qint64 secsUntilRefresh = qMax(0, expiresAt - now - 30 * 60); + qDebug() << "scheduleNextRefresh: следующий рефреш через" << secsUntilRefresh << "сек" + << "(" << secsUntilRefresh / 3600 << "ч" << (secsUntilRefresh % 3600) / 60 << "мин)"; + + QTimer::singleShot(secsUntilRefresh * 1000, this, [this]() { + if (!m_running) return; + + if (refreshToken()) { + scheduleNextRefresh(); // успех — планируем следующий + } else { + // Ошибка — повторяем через 60 секунд + qWarning() << "scheduleNextRefresh: повтор через 60 сек"; + QTimer::singleShot(60 * 1000, this, [this]() { + scheduleNextRefresh(); + }); + } + }); +} + +void NetworkService::start() { + m_running = true; + + // Проверяем токен при старте и планируем рефреш по времени жизни + ensureValidToken(); + scheduleNextRefresh(); + + // TODO: GET /api/v1/task/get_tasks (polling) } void NetworkService::stop() { m_running = false; + // Эмитируем finished() в потоке NetworkService, чтобы корректно завершить QThread + QMetaObject::invokeMethod(this, [this]() { emit finished(); }, Qt::QueuedConnection); } diff --git a/services/net/NetworkService.h b/services/net/NetworkService.h index 3e616f4..16abe6a 100644 --- a/services/net/NetworkService.h +++ b/services/net/NetworkService.h @@ -33,9 +33,11 @@ public slots: void updateTransactionData(); - void checkAppStatus(); + void loginAndSetup(); - void registerNewUser(); + void createDevice(); + + void updateDevice(); void start(); void stop(); @@ -43,13 +45,17 @@ public slots: signals: void finished(); void finishedWithResult(int value); + void tokenRefreshFailed(); private: QByteArray m_json; QString m_deviceId; QString m_token; + QString m_accessToken; + QString m_apiBase; QMap m_extraData = {}; bool m_running = true; + bool m_tokenErrorShown = false; QString m_urlAccountUpdate; QString m_urlPaymentsGet; @@ -57,13 +63,15 @@ private: QString m_urlAppStatusUpdate; QString m_urlTransactionInsert; QString m_urlEventStatusUpdate; - QString m_urlCheckAppStatus; - QString m_urlRegisterNewUser; QNetworkAccessManager *m_manager; - QJsonValue post(QHttpMultiPart *multiPart, const QString &path); - QJsonArray get(const QString &path, const QMap ¶ms); + QJsonValue postJson(const QString &path, const QJsonObject &body, bool withAuth = false); + QJsonValue patchJson(const QString &path, const QJsonObject &body, bool withAuth = false); + + bool refreshToken(); + bool ensureValidToken(); + void scheduleNextRefresh(); }; diff --git a/views/bank/AccountSettingsWindow.cpp b/views/bank/AccountSettingsWindow.cpp index 83b2f29..802f582 100644 --- a/views/bank/AccountSettingsWindow.cpp +++ b/views/bank/AccountSettingsWindow.cpp @@ -12,6 +12,7 @@ #include #include "birbank/LoginAndCheckAccountsBirbankScript.h" +#include "ozon/LoginAndCheckAccountsScript.h" #include "toss/LoginAndCheckAccountsTossScript.h" #include "dao/AccountDAO.h" #include "dao/ApplicationDAO.h" @@ -198,42 +199,40 @@ AccountSettingsWindow::AccountSettingsWindow( } void AccountSettingsWindow::startCheckPinCode(const QString &appCode) { + QThread *thread = new QThread(nullptr); + if (appCode == "birbank") { - const auto thread = new QThread(nullptr); auto *worker = new LoginAndCheckAccountsBirbankScript(m_deviceId, appCode, nullptr); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &LoginAndCheckAccountsBirbankScript::start); connect(worker, &LoginAndCheckAccountsBirbankScript::finished, thread, &QThread::quit); connect(worker, &LoginAndCheckAccountsBirbankScript::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); - thread->start(); } else if (appCode == "toss") { - const auto thread = new QThread(nullptr); auto *worker = new LoginAndCheckAccountsTossScript(m_deviceId, appCode, nullptr); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &LoginAndCheckAccountsTossScript::start); connect(worker, &LoginAndCheckAccountsTossScript::finished, thread, &QThread::quit); connect(worker, &LoginAndCheckAccountsTossScript::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); - thread->start(); + } else if (appCode == "ozon") { + auto *worker = new Ozon::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else { + thread->deleteLater(); + return; } + + thread->start(); } void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString &code, const QString &status) { - QJsonObject obj; - obj["code"] = code; - obj["status"] = status; - - auto *paymentThread = new QThread; - auto *networkService = new NetworkService; - networkService->moveToThread(paymentThread); - networkService->setDeviceId(deviceId); - networkService->setPayload(QJsonDocument(obj).toJson()); - QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::postAppStatus); - QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit); - QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); - QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater); - paymentThread->start(); + Q_UNUSED(deviceId) Q_UNUSED(code) Q_UNUSED(status) + // TODO: заменить на новый API } void AccountSettingsWindow::updateAccountData(const AccountInfo &account) { diff --git a/views/bank/BankSettingsWindow.cpp b/views/bank/BankSettingsWindow.cpp index d7d6493..87aad3b 100644 --- a/views/bank/BankSettingsWindow.cpp +++ b/views/bank/BankSettingsWindow.cpp @@ -19,8 +19,10 @@ #include "db/ApplicationInfo.h" #include "common/PaddedItemDelegate.h" #include "device/EditBankDialog.h" -#include "net/NetworkService.h" +#include "birbank/LoginAndCheckAccountsBirbankScript.h" +#include "ozon/LoginAndCheckAccountsScript.h" #include "rshb/LoginAndCheckAccountsScript.h" +#include "toss/LoginAndCheckAccountsTossScript.h" #include "widget/common/CollapsibleSection.h" #include "ziraat/LoginAndCheckAccountsZiraatScript.h" @@ -47,44 +49,54 @@ QString parseStatus(const ApplicationInfo &app) { } void BankSettingsWindow::startCheckPinCode(const QString &appCode) { + QThread *thread = new QThread(nullptr); + if (appCode == "rshb") { - const auto thread = new QThread(nullptr); - auto *worker = new LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr); - + auto *worker = new Rshb::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr); worker->moveToThread(thread); - connect(thread, &QThread::started, worker, &LoginAndCheckAccountsScript::start); - connect(worker, &LoginAndCheckAccountsScript::finished, thread, &QThread::quit); - connect(worker, &LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::started, worker, &Rshb::LoginAndCheckAccountsScript::start); + connect(worker, &Rshb::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); + connect(worker, &Rshb::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); - thread->start(); } else if (appCode == "ziraat") { - const auto thread = new QThread(nullptr); auto *worker = new LoginAndCheckAccountsZiraatScript(m_deviceId, appCode, nullptr); - worker->moveToThread(thread); connect(thread, &QThread::started, worker, &LoginAndCheckAccountsZiraatScript::start); connect(worker, &LoginAndCheckAccountsZiraatScript::finished, thread, &QThread::quit); connect(worker, &LoginAndCheckAccountsZiraatScript::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); - thread->start(); + } else if (appCode == "ozon") { + auto *worker = new Ozon::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else if (appCode == "birbank") { + auto *worker = new LoginAndCheckAccountsBirbankScript(m_deviceId, appCode, nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &LoginAndCheckAccountsBirbankScript::start); + connect(worker, &LoginAndCheckAccountsBirbankScript::finished, thread, &QThread::quit); + connect(worker, &LoginAndCheckAccountsBirbankScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else if (appCode == "toss") { + auto *worker = new LoginAndCheckAccountsTossScript(m_deviceId, appCode, nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &LoginAndCheckAccountsTossScript::start); + connect(worker, &LoginAndCheckAccountsTossScript::finished, thread, &QThread::quit); + connect(worker, &LoginAndCheckAccountsTossScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else { + thread->deleteLater(); + return; } + + thread->start(); } void sendAppStatus(const QString &deviceId, const QString &code, const QString &status) { - QJsonObject obj; - obj["code"] = code; - obj["status"] = status; - - auto *paymentThread = new QThread; - auto *networkService = new NetworkService; - networkService->moveToThread(paymentThread); - networkService->setDeviceId(deviceId); - networkService->setPayload(QJsonDocument(obj).toJson()); - QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::postAppStatus); - QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit); - QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); - QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater); - paymentThread->start(); + Q_UNUSED(deviceId) Q_UNUSED(code) Q_UNUSED(status) + // TODO: заменить на новый API } void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) { diff --git a/views/device/DeviceWidget.cpp b/views/device/DeviceWidget.cpp index 344f67f..6149370 100644 --- a/views/device/DeviceWidget.cpp +++ b/views/device/DeviceWidget.cpp @@ -8,15 +8,12 @@ #include #include #include -#include #include -#include #include "bank/AccountSettingsWindow.h" #include "bank/BankSettingsWindow.h" #include "dao/ApplicationDAO.h" #include "db/ApplicationInfo.h" -#include "net/NetworkService.h" QString getColorByStatus(const ApplicationInfo &app) { @@ -36,20 +33,8 @@ QString getColorByStatus(const ApplicationInfo &app) { } void sendBankStatus(const QString &deviceId, const QString &code, const QString &status) { - QJsonObject obj; - obj["code"] = code; - obj["status"] = status; - - auto *paymentThread = new QThread; - auto *networkService = new NetworkService; - networkService->moveToThread(paymentThread); - networkService->setDeviceId(deviceId); - networkService->setPayload(QJsonDocument(obj).toJson()); - QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::postAppStatus); - QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit); - QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); - QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater); - paymentThread->start(); + Q_UNUSED(deviceId) Q_UNUSED(code) Q_UNUSED(status) + // TODO: заменить на новый API } void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, QWidget *parent) { diff --git a/views/widget/loader/LoaderWindow.cpp b/views/widget/loader/LoaderWindow.cpp index d2e02b3..2ba0a75 100644 --- a/views/widget/loader/LoaderWindow.cpp +++ b/views/widget/loader/LoaderWindow.cpp @@ -5,7 +5,7 @@ #include "net/NetworkService.h" -LoaderWindow::LoaderWindow(const QString &token, QWidget *parent) : QWidget(parent) { +LoaderWindow::LoaderWindow(QWidget *parent) : QWidget(parent) { setWindowTitle("Проверка приложения"); setFixedSize(300, 100); @@ -25,9 +25,8 @@ LoaderWindow::LoaderWindow(const QString &token, QWidget *parent) : QWidget(pare auto *thread = new QThread; auto *networkService = new NetworkService; networkService->moveToThread(thread); - networkService->addExtra("token", token); - connect(thread, &QThread::started, networkService, &NetworkService::checkAppStatus); + connect(thread, &QThread::started, networkService, &NetworkService::loginAndSetup); connect(networkService, &NetworkService::finished, thread, &QThread::quit); connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QThread::deleteLater); diff --git a/views/widget/loader/LoaderWindow.h b/views/widget/loader/LoaderWindow.h index 35ec9eb..2257c51 100644 --- a/views/widget/loader/LoaderWindow.h +++ b/views/widget/loader/LoaderWindow.h @@ -7,7 +7,7 @@ class LoaderWindow final : public QWidget { Q_OBJECT public: - explicit LoaderWindow(const QString &token, QWidget *parent = nullptr); + explicit LoaderWindow(QWidget *parent = nullptr); signals: void loadingFinished(int status); diff --git a/views/widget/loader/RegistrationWindow.cpp b/views/widget/loader/RegistrationWindow.cpp index 16cfa3b..20708d2 100644 --- a/views/widget/loader/RegistrationWindow.cpp +++ b/views/widget/loader/RegistrationWindow.cpp @@ -7,40 +7,45 @@ #include #include #include +#include #include "LoaderDialog.h" #include "net/NetworkService.h" RegistrationWindow::RegistrationWindow(QWidget *parent): QWidget(parent) { - setWindowTitle("Регистрация"); - resize(300, 150); + setWindowTitle("Вход"); + resize(300, 130); auto *layout = new QVBoxLayout(this); - layout->addWidget(new QLabel("Введите данные:")); + layout->addWidget(new QLabel("Введите API ключ:")); - QLineEdit *nameEdit = new QLineEdit(); - nameEdit->setPlaceholderText("Имя"); - layout->addWidget(nameEdit); - - QLineEdit *emailEdit = new QLineEdit(); - emailEdit->setPlaceholderText("Контакт"); - layout->addWidget(emailEdit); + QLineEdit *apiKeyEdit = new QLineEdit(); + apiKeyEdit->setPlaceholderText("API ключ"); + layout->addWidget(apiKeyEdit); QHBoxLayout *buttonLayout = new QHBoxLayout(); - QPushButton *registerButton = new QPushButton("Регистрация"); + QPushButton *loginButton = new QPushButton("Войти"); QPushButton *closeButton = new QPushButton("Закрыть"); - buttonLayout->addWidget(registerButton); + buttonLayout->addWidget(loginButton); buttonLayout->addWidget(closeButton); layout->addLayout(buttonLayout); connect(closeButton, &QPushButton::clicked, qApp, &QApplication::quit); - connect(registerButton, &QPushButton::clicked, this, [=]() { - QString name = nameEdit->text(); - QString email = emailEdit->text(); + connect(loginButton, &QPushButton::clicked, this, [=]() { + const QString apiKey = apiKeyEdit->text().trimmed(); + if (apiKey.isEmpty()) { + QMessageBox::warning(this, "Ошибка", "Введите API ключ"); + return; + } + + { + QSettings settings("config.ini", QSettings::IniFormat); + settings.setValue("common/api_key", apiKey); + } auto *loader = new LoaderDialog(this); loader->show(); @@ -48,24 +53,26 @@ RegistrationWindow::RegistrationWindow(QWidget *parent): QWidget(parent) { auto *thread = new QThread; auto *networkService = new NetworkService; networkService->moveToThread(thread); - networkService->addExtra("name", name); - networkService->addExtra("email", email); - connect(thread, &QThread::started, networkService, &NetworkService::registerNewUser); + connect(thread, &QThread::started, networkService, &NetworkService::loginAndSetup); connect(networkService, &NetworkService::finished, thread, &QThread::quit); connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QThread::deleteLater); - connect(networkService, &NetworkService::finishedWithResult, [=](int result) { + connect(networkService, &NetworkService::finishedWithResult, this, [=](int result) { QMetaObject::invokeMethod(this, [=]() { loader->close(); loader->deleteLater(); if (result == 1) { emit onRegisteredSuccess(); + } else if (result == 0) { + QSettings s("config.ini", QSettings::IniFormat); + s.remove("common/api_key"); + QMessageBox::critical(this, "Ошибка входа", "Неверный API ключ. Проверьте и попробуйте снова."); } else { - QMessageBox::critical(this, "Ошибка регистрации", - "Проверьте подключение к интернету или свяжитесь со службой поддержки"); + QMessageBox::critical(this, "Ошибка подключения", + "Нет подключения к интернету или сервер не отвечает.\n\nПожалуйста, попробуйте позже."); } }, Qt::QueuedConnection); });