Add ScreenXmlZiraatParser for parsing Ziraat XML data
Introduce a new class, `ScreenXmlZiraatParser`, to handle Ziraat-related XML parsing. This includes functionality to extract transaction details, banners/dialogs recognition, and history parsing for today and yesterday. Regular expressions are utilized for precise data extraction.
This commit is contained in:
parent
748d515fbf
commit
ab16b8962f
292
android/ziraat/CommonZiraatScript.cpp
Normal file
292
android/ziraat/CommonZiraatScript.cpp
Normal file
@ -0,0 +1,292 @@
|
||||
#include "CommonZiraatScript.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QRandomGenerator>
|
||||
#include <QThread>
|
||||
|
||||
#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<Node> &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<Node> 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<QPair<AccountInfo, Node> > 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<QPair<AccountInfo, Node> > &accounts) {
|
||||
if (accounts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *thread = new QThread;
|
||||
auto *worker = new NetworkService;
|
||||
|
||||
QJsonArray list;
|
||||
for (const QPair<AccountInfo, Node> &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;
|
||||
}
|
||||
73
android/ziraat/CommonZiraatScript.h
Normal file
73
android/ziraat/CommonZiraatScript.h
Normal file
@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#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<Node> &pincode);
|
||||
|
||||
bool goToAllProducts(
|
||||
const QString &deviceId,
|
||||
int width,
|
||||
int height
|
||||
);
|
||||
|
||||
bool findAndTapOnAccount(const QString &deviceId, const QString &accountNumbers);
|
||||
|
||||
void postAccountsData(const QList<QPair<AccountInfo, Node> > &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);
|
||||
};
|
||||
336
android/ziraat/PayByIbanScript.cpp
Normal file
336
android/ziraat/PayByIbanScript.cpp
Normal file
@ -0,0 +1,336 @@
|
||||
#include "PayByIbanScript.h"
|
||||
|
||||
#include <QRandomGenerator>
|
||||
#include <QThread>
|
||||
#include <utility>
|
||||
|
||||
#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<Node> 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<QPair<AccountInfo, Node> > 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);
|
||||
}
|
||||
34
android/ziraat/PayByIbanScript.h
Normal file
34
android/ziraat/PayByIbanScript.h
Normal file
@ -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
|
||||
);
|
||||
};
|
||||
976
android/ziraat/ScreenXmlZiraatParser.cpp
Normal file
976
android/ziraat/ScreenXmlZiraatParser.cpp
Normal file
@ -0,0 +1,976 @@
|
||||
#include "ScreenXmlZiraatParser.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QDateTime>
|
||||
|
||||
#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()) {
|
||||
// <node class="android.widget.Button" package="ru.rshb.dbo" clickable="true" bounds="[630,142][691,205]"/>
|
||||
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<Node> ScreenXmlZiraatParser::findPinCodeNodes(const QString &pinCode) {
|
||||
QMap<QString, Node> buttons;
|
||||
const QSet<QString> 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<Node> nodes;
|
||||
for (int i = 0; i < pinCode.length(); ++i) {
|
||||
nodes.append(buttons[QString(pinCode.at(i))]);
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
QList<Node> 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<Node> ScreenXmlZiraatParser::getLast2DaysTransactions() {
|
||||
QList<Node> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* <node NAF="true" index="1" text="" resource-id=""
|
||||
class="android.widget.Button" package="ru.rshb.dbo"
|
||||
content-desc="" checkable="false" checked="false"
|
||||
clickable="true" enabled="true" focusable="false"
|
||||
focused="false" scrollable="false"
|
||||
long-clickable="false" password="false"
|
||||
selected="false" bounds="[952,159][1039,248]"/>
|
||||
*/
|
||||
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<TransactionInfo> ScreenXmlZiraatParser::parseTodayAndYesterdayReportHistory() {
|
||||
QList<TransactionInfo> 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<TransactionInfo> 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<TransactionInfo> 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<QPair<AccountInfo, Node> > ScreenXmlZiraatParser::parseAccountsInfo() {
|
||||
QList<QPair<AccountInfo, Node> > 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);
|
||||
// ЕСПП
|
||||
}
|
||||
57
android/ziraat/ScreenXmlZiraatParser.h
Normal file
57
android/ziraat/ScreenXmlZiraatParser.h
Normal file
@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QDomElement>
|
||||
#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<Node> tryToFindPinCode(const QString &pinCode, const QString &title);
|
||||
|
||||
QList<Node> getLast2DaysTransactions();
|
||||
|
||||
void parseAndSaveXml(const QString &xml);
|
||||
|
||||
TransactionInfo parseTransactionInfoHistory(int screenWidth, int screenHeight);
|
||||
TransactionInfo parseTransactionInfo(int screenWidth, int screenHeight);
|
||||
|
||||
QList<TransactionInfo> parseTodayAndYesterdayReportHistory();
|
||||
|
||||
QList<TransactionInfo> parseTodayAndYesterdayHistory();
|
||||
|
||||
|
||||
QList<QPair<AccountInfo, Node> > parseAccountsInfo();
|
||||
|
||||
void test();
|
||||
|
||||
private:
|
||||
QList<Node> m_nodes;
|
||||
QDomElement m_xml;
|
||||
|
||||
QList<Node> findPinCodeNodes(const QString &pinCode);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user