diff --git a/android/ziraat/CommonZiraatScript.cpp b/android/ziraat/CommonZiraatScript.cpp new file mode 100644 index 0000000..059c1c7 --- /dev/null +++ b/android/ziraat/CommonZiraatScript.cpp @@ -0,0 +1,292 @@ +#include "CommonZiraatScript.h" + +#include +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "net/NetworkService.h" + +CommonZiraatScript::CommonZiraatScript(QObject *parent) + : QObject(parent) { +} + +CommonZiraatScript::~CommonZiraatScript() = default; + +void CommonZiraatScript::start() { + doStart(); + emit finished(); +} + +Node CommonZiraatScript::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 CommonZiraatScript::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 CommonZiraatScript::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 CommonZiraatScript::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 CommonZiraatScript::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)); + } + + if (xmlScreenParser.isHomeScreen()) { + // идем на экран "Все продукты" + if (Node allCardTitle = xmlScreenParser.findTextNode("Счета и карты"); !allCardTitle.isEmpty()) { + // 1. Если нет кнопки видимой, свайпаем. Кликаем на "Все продукты" + for (int i = 0; i < 5; ++i) { + Node allProductsBtn = xmlScreenParser.findButtonNode("Все продукты", false); + if (allProductsBtn.x() > 0 && allProductsBtn.y() > 0 && allProductsBtn.y() < height - 200) { + AdbUtils::makeTap(deviceId, allProductsBtn.x(), allProductsBtn.y()); + QThread::msleep(1000); + break; + } + AdbUtils::makeSwipe(deviceId, + allCardTitle.x(), allCardTitle.y(), + allCardTitle.x(), allCardTitle.y() - 200); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + + for (int i = 0; i < 5; ++i) { + Node allProductsTitle = xmlScreenParser.findTextNode("Все продукты"); + // Проверяем что вверху заголовок + if (!allProductsTitle.isEmpty() && allProductsTitle.y() < 300) { + return true; + } + + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + } else { + // FIXME пишем в БД, что не дошли до домашней страницы, и завершаемся + // сохраняем скрин приложения + // дделаем алерт + } + } else { + // FIXME + qDebug() << "Not home screen..."; + } + return false; +} + +bool CommonZiraatScript::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 CommonZiraatScript::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["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 CommonZiraatScript::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 CommonZiraatScript::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 CommonZiraatScript::stop() { + m_running = false; +} diff --git a/android/ziraat/CommonZiraatScript.h b/android/ziraat/CommonZiraatScript.h new file mode 100644 index 0000000..d7d2b7b --- /dev/null +++ b/android/ziraat/CommonZiraatScript.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include "ScreenXmlZiraatParser.h" + +class CommonZiraatScript : public QObject { + Q_OBJECT + +public: + explicit CommonZiraatScript(QObject *parent = nullptr); + + ~CommonZiraatScript() override; + +protected: + virtual void doStart() = 0; + + 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; + ScreenXmlZiraatParser 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(CommonZiraatScript); +}; diff --git a/android/ziraat/PayByIbanScript.cpp b/android/ziraat/PayByIbanScript.cpp new file mode 100644 index 0000000..5df1df7 --- /dev/null +++ b/android/ziraat/PayByIbanScript.cpp @@ -0,0 +1,336 @@ +#include "PayByIbanScript.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" + +PayByIbanScript::PayByIbanScript( + AccountInfo account, + QString phone, + QString bankName, + const double amount, + QObject *parent +) : CommonZiraatScript(parent), + m_account(std::move(account)), m_phone(std::move(phone)), + m_bankName(std::move(bankName)), m_amount(amount) { +} + +PayByIbanScript::~PayByIbanScript() = default; + + +void PayByIbanScript::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 PayByIbanScript::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::Blocked; + 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); +} diff --git a/android/ziraat/PayByIbanScript.h b/android/ziraat/PayByIbanScript.h new file mode 100644 index 0000000..8713ee1 --- /dev/null +++ b/android/ziraat/PayByIbanScript.h @@ -0,0 +1,34 @@ +#pragma once +#include "CommonZiraatScript.h" + +class PayByIbanScript final : public CommonZiraatScript { + Q_OBJECT + +public: + ~PayByIbanScript() override; + + PayByIbanScript( + 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 + ); +}; diff --git a/android/ziraat/ScreenXmlZiraatParser.cpp b/android/ziraat/ScreenXmlZiraatParser.cpp new file mode 100644 index 0000000..4e3caef --- /dev/null +++ b/android/ziraat/ScreenXmlZiraatParser.cpp @@ -0,0 +1,976 @@ +#include "ScreenXmlZiraatParser.h" + +#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" + +ScreenXmlZiraatParser::ScreenXmlZiraatParser(QObject *parent) : QObject(parent) { +} + +ScreenXmlZiraatParser::~ScreenXmlZiraatParser() = 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 ScreenXmlZiraatParser::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 ScreenXmlZiraatParser::tryToFindSecurityBanner() { + Node text; + Node closeBtn; + 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 (!text.isEmpty() && !closeBtn.isEmpty()) { + return closeBtn; + } + return {}; +} + +Node ScreenXmlZiraatParser::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 ScreenXmlZiraatParser::findTextNode(const QString &text) { + for (const Node &node: m_nodes) { + if (node.className == "android.widget.TextView" && node.text.trimmed() == text) { + return node; + } + } + return {}; +} + +Node ScreenXmlZiraatParser::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 ScreenXmlZiraatParser::findFirstEditText() { + for (const Node &node: m_nodes) { + if (node.className == "android.widget.EditText" && node.focusable) { + return node; + } + } + return {}; +} + +Node ScreenXmlZiraatParser::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 {}; +} + +bool ScreenXmlZiraatParser::isHomeScreen() { + // Есть заголовок "Счета и карты" и кнопка "Все продукты" + Node text; + Node allProductBtn; + for (const Node &node: m_nodes) { + if (node.text.trimmed() == "Счета и карты" && node.className == "android.widget.TextView") { + text = node; + } + if (node.text.trimmed() == "Все продукты" && node.className == "android.widget.Button") { + allProductBtn = node; + } + } + return !text.isEmpty() && !allProductBtn.isEmpty(); +} + +QList ScreenXmlZiraatParser::findPinCodeNodes(const QString &pinCode) { + QMap buttons; + const QSet numberSet = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; + + for (const Node &node: m_nodes) { + const QString &text = node.text.trimmed(); + if (node.className == "android.widget.Button" && numberSet.contains(text)) { + buttons[text] = node; + } + } + + QList nodes; + for (int i = 0; i < pinCode.length(); ++i) { + nodes.append(buttons[QString(pinCode.at(i))]); + } + return nodes; +} + +QList ScreenXmlZiraatParser::tryToFindPinCode(const QString &pinCode, const QString &title) { + for (const Node &node: m_nodes) { + // если там есть такая кнопка значит банер есть, и пин-код не прожать + if (node.text.trimmed() == "Отложить" && node.className == "android.widget.Button") { + return {}; + } + } + for (const Node &node: m_nodes) { + // FIXME определение пин-кода лучше сделать + if (node.text.trimmed() == title) { + return findPinCodeNodes(pinCode); + } + } + return {}; +} + +QList ScreenXmlZiraatParser::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 ScreenXmlZiraatParser::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 ScreenXmlZiraatParser::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 ScreenXmlZiraatParser::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 ScreenXmlZiraatParser::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 ScreenXmlZiraatParser::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 > ScreenXmlZiraatParser::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 ScreenXmlZiraatParser::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); + // ЕСПП +} diff --git a/android/ziraat/ScreenXmlZiraatParser.h b/android/ziraat/ScreenXmlZiraatParser.h new file mode 100644 index 0000000..8f64796 --- /dev/null +++ b/android/ziraat/ScreenXmlZiraatParser.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include "android/xml/Node.h" +#include "db/AccountInfo.h" +#include "db/TransactionInfo.h" + +class ScreenXmlZiraatParser final : public QObject { + Q_OBJECT + +public: + explicit ScreenXmlZiraatParser(QObject *parent = nullptr); + + ~ScreenXmlZiraatParser() 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); + + 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); +};