1
0
forked from BRT/arc
arc/android/ozon/PayByPhoneScript.cpp

499 lines
22 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "PayByPhoneScript.h"
#include <QProcess>
#include <QRegularExpression>
#include <QThread>
#include <QTimeZone>
#include <utility>
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include "adb/AdbUtils.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "time/DateUtils.h"
#include <QCoreApplication>
static QString recognizeTextFromImage(const QByteArray &pngData) {
auto *api = new tesseract::TessBaseAPI();
if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "rus+eng")) {
qWarning() << "[OCR] Failed to init Tesseract";
delete api;
return {};
}
Pix *pix = pixReadMem(reinterpret_cast<const l_uint8 *>(pngData.constData()),
static_cast<size_t>(pngData.size()));
if (!pix) {
qWarning() << "[OCR] Failed to read image";
api->End();
delete api;
return {};
}
api->SetImage(pix);
char *text = api->GetUTF8Text();
QString result = QString::fromUtf8(text).trimmed();
delete[] text;
pixDestroy(&pix);
api->End();
delete api;
return result;
}
static QString extractTransactionId(const QString &ocrText) {
// "ID операции A60130542076330K0000040011670301" или "ID операции = Аб60650..."
QRegularExpression reId(R"(ID\s+операции\s*=?\s*([A-Za-zА-Яа-я0-9]+))");
if (auto m = reId.match(ocrText); m.hasMatch()) return m.captured(1);
// "№ Ф-2026-25235663"
QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))");
if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1);
return {};
}
namespace Ozon {
PayByPhoneScript::PayByPhoneScript(
MaterialInfo account,
QString phone,
QString bankName,
const double amount,
QObject *parent
) : CommonScript(parent),
m_account(std::move(account)), m_phone(std::move(phone)),
m_bankName(std::move(bankName)), m_amount(amount) {
}
PayByPhoneScript::~PayByPhoneScript() = default;
void PayByPhoneScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId);
const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_account.deviceId;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
// Закрываем рекламный bottom sheet
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
qDebug() << "[PayByPhone] Ad banner detected, closing...";
AdbUtils::makeTap(device.id, adNode.x(), adNode.y());
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
// Если не на домашнем экране — перезапускаем
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[PayByPhone] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.adbSerial, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + device.id;
qWarning() << m_error;
AdbUtils::tryToKillApplication(device.adbSerial, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
// Закрываем баннеры
tryToCloseAllBannersOnHomePage(device.id, device.screenWidth, device.screenHeight);
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
// Закрываем баннер Google Play "Доступно обновление"
if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(); !gpNode.isEmpty()) {
qDebug() << "[PayByPhone] Google Play update banner detected, closing...";
AdbUtils::makeTap(device.id, gpNode.x(), gpNode.y());
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
// Закрываем баннер "Заказать карту" — тапаем сверху экрана
if (Node orderCardNode = xmlScreenParser.findTextNode("Заказать карту"); !orderCardNode.isEmpty()) {
qDebug() << "[PayByPhone] 'Заказать карту' banner detected, closing...";
AdbUtils::makeTap(device.id, device.screenWidth / 2, device.screenHeight / 20);
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
makePayment(device.adbSerial, app.pinCode, device.screenWidth, device.screenHeight);
emit finishedWithResult(m_error);
}
void PayByPhoneScript::makePayment(
const QString &deviceId,
const QString &pinCode,
const int width,
const int height
) {
// 1. Нажимаем "Перевести" на домашнем экране
Node transferBtn = xmlScreenParser.findButtonNode("Перевести", false);
if (transferBtn.isEmpty()) {
m_error = "Кнопка 'Перевести' не найдена на домашнем экране";
qWarning() << "[PayByPhone]" << m_error;
return;
}
qDebug() << "[PayByPhone] Tapping 'Перевести'...";
AdbUtils::makeTap(deviceId, transferBtn.x(), transferBtn.y());
QThread::msleep(2000);
// 2. Ждём экран "Платежи и переводы" с полем "Телефон получателя"
bool paymentsPageLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node phoneField = xmlScreenParser.findFirstEditText();
if (!phoneField.isEmpty()) {
paymentsPageLoaded = true;
break;
}
qDebug() << "[PayByPhone] Waiting for payments page, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!paymentsPageLoaded) {
m_error = "Экран 'Платежи и переводы' не загрузился";
qWarning() << "[PayByPhone]" << m_error;
return;
}
// 3. Тапаем на поле "Телефон получателя" чтобы перейти на экран "Кому перевести"
Node phoneFieldTap = xmlScreenParser.findFirstEditText();
qDebug() << "[PayByPhone] Tapping 'Телефон получателя'...";
AdbUtils::makeTap(deviceId, phoneFieldTap.x(), phoneFieldTap.y());
QThread::msleep(2000);
// 4. Ждём экран "Кому перевести"
bool phoneScreenLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node title = xmlScreenParser.findTextNode("Кому перевести");
if (!title.isEmpty()) {
phoneScreenLoaded = true;
break;
}
qDebug() << "[PayByPhone] Waiting for 'Кому перевести', attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!phoneScreenLoaded) {
m_error = "Экран 'Кому перевести' не загрузился";
qWarning() << "[PayByPhone]" << m_error;
return;
}
// 5. Тапаем на EditText и вводим номер телефона
Node phoneInput;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
phoneInput = xmlScreenParser.findFirstEditText();
if (!phoneInput.isEmpty()) break;
qDebug() << "[PayByPhone] Waiting for phone input field, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (phoneInput.isEmpty()) {
m_error = "Поле ввода телефона не найдено";
qWarning() << "[PayByPhone]" << m_error;
return;
}
AdbUtils::makeTap(deviceId, phoneInput.x(), phoneInput.y());
QThread::msleep(500);
AdbUtils::inputText(deviceId, m_phone);
QThread::msleep(2000);
// 6. Ждём экран "По номеру телефона", обрабатывая промежуточные экраны
bool sumScreenLoaded = false;
bool bankSelected = false;
bool otherBankClicked = false;
for (int attempt = 0; attempt < 20; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
// Проверяем: уже на экране суммы?
Node sumTitle = xmlScreenParser.findTextNode("По номеру телефона");
if (!sumTitle.isEmpty() && sumTitle.y() < height / 7) {
sumScreenLoaded = true;
break;
}
// Ищем кнопку с номером телефона (новый перевод / последние переводы)
QString phoneDigits = m_phone;
phoneDigits.remove(QRegularExpression("[^0-9]"));
bool foundPhoneBtn = false;
for (const Node &btn : xmlScreenParser.findAllButtons()) {
QString btnDigits = btn.text;
btnDigits.remove(QRegularExpression("[^0-9]"));
if (btnDigits.contains(phoneDigits) || phoneDigits.contains(btnDigits.right(10))) {
qDebug() << "[PayByPhone] Found button with phone number:" << btn.text;
AdbUtils::makeTap(deviceId, btn.x(), btn.y());
QThread::msleep(2000);
foundPhoneBtn = true;
break;
}
}
if (foundPhoneBtn) continue;
// Экран выбора банка: ищем "Другой банк" (текст) или EditText с hint "Название банка"
if (!bankSelected) {
// Вариант 1: кнопка "Другой банк" (content-desc="other-bank-item", text пустой)
bool foundOtherBank = false;
{
Node node;
for (const Node &n : xmlScreenParser.findAllNodes()) {
if (n.contentDesc.contains("other-bank-item")) {
node = n;
break;
}
}
if (!node.isEmpty()) {
qDebug() << "[PayByPhone] Tapping 'Другой банк' (other-bank-item)...";
AdbUtils::makeTap(deviceId, node.x(), node.y());
QThread::msleep(2000);
foundOtherBank = true;
otherBankClicked = true;
}
}
if (foundOtherBank) continue;
// Вариант 2: уже на экране поиска банка (EditText) — только после клика на "Другой банк"
if (otherBankClicked) {
Node bankInput = xmlScreenParser.findFirstEditText();
if (!bankInput.isEmpty()) {
qDebug() << "[PayByPhone] Bank search screen detected, entering bank name...";
AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y());
QThread::msleep(500);
// Ввод кириллицы через ADB Keyboard
AdbUtils::enableAdbIMEKeyboard(deviceId);
QThread::msleep(500);
AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y());
QThread::msleep(500);
AdbUtils::inputAdbIMEText(deviceId, m_bankName);
QThread::msleep(2000);
AdbUtils::disableAdbIMEKeyboard(deviceId);
qDebug() << "[PayByPhone] Entered bank name via ADB IME:" << m_bankName;
QThread::msleep(2000);
// Ищем банк в списке и кликаем по нему (кнопка с числовым content-desc)
bool bankFound = false;
for (int ba = 0; ba < 10; ++ba) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
for (const Node &node : xmlScreenParser.findAllNodes()) {
if (node.className == "android.widget.Button"
&& !node.contentDesc.isEmpty()
&& node.contentDesc != "base-go-back-button"
&& node.contentDesc != "native-toolbar-close-button"
&& node.contentDesc != "clear-input-icon") {
qDebug() << "[PayByPhone] Found bank in list, tapping:" << node.contentDesc << "bounds:" << node.x() << node.y();
AdbUtils::makeTap(deviceId, node.x(), node.y());
bankFound = true;
break;
}
}
if (bankFound) break;
qDebug() << "[PayByPhone] Waiting for bank list, attempt" << ba + 1 << "/ 10";
QThread::msleep(1500);
}
if (!bankFound) {
m_error = "Банк '" + m_bankName + "' не найден в списке";
qWarning() << "[PayByPhone]" << m_error;
return;
}
bankSelected = true;
QThread::msleep(2000);
continue;
}
} // otherBankClicked
}
qDebug() << "[PayByPhone] Waiting for 'По номеру телефона', attempt" << attempt + 1 << "/ 20";
QThread::msleep(1500);
}
if (!sumScreenLoaded) {
m_error = "Экран ввода суммы не загрузился";
qWarning() << "[PayByPhone]" << m_error;
return;
}
// 7. Вводим сумму
Node sumInput = xmlScreenParser.findFirstEditText();
if (sumInput.isEmpty()) {
m_error = "Поле ввода суммы не найдено";
qWarning() << "[PayByPhone]" << m_error;
return;
}
AdbUtils::makeTap(deviceId, sumInput.x(), sumInput.y());
QThread::msleep(500);
AdbUtils::inputText(deviceId, QString::number(static_cast<int>(m_amount)));
QThread::msleep(1000);
// 6. Нажимаем "Продолжить"
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node continueBtn = xmlScreenParser.findButtonNode("Продолжить", false);
if (continueBtn.isEmpty()) {
m_error = "Кнопка 'Продолжить' не найдена";
qWarning() << "[PayByPhone]" << m_error;
return;
}
AdbUtils::makeTap(deviceId, continueBtn.x(), continueBtn.y());
QThread::msleep(2000);
// 7. Ждём экран "Подтвердите перевод" с кнопкой "Перевести ..."
bool confirmScreenLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node confirmTitle = xmlScreenParser.findTextNode("Подтвердите перевод");
if (!confirmTitle.isEmpty()) {
confirmScreenLoaded = true;
break;
}
qDebug() << "[PayByPhone] Waiting for 'Подтвердите перевод', attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!confirmScreenLoaded) {
m_error = "Экран подтверждения не загрузился";
qWarning() << "[PayByPhone]" << m_error;
return;
}
// 8. Нажимаем "Перевести ..." (ищем по первому слову)
Node transferConfirmBtn = xmlScreenParser.findButtonNode("Перевести", true);
if (transferConfirmBtn.isEmpty()) {
m_error = "Кнопка 'Перевести ...' не найдена на экране подтверждения";
qWarning() << "[PayByPhone]" << m_error;
return;
}
qDebug() << "[PayByPhone] Tapping confirm button:" << transferConfirmBtn.text;
AdbUtils::makeTap(deviceId, transferConfirmBtn.x(), transferConfirmBtn.y());
QThread::msleep(3000);
// 8.5. Проверяем экран ввода PIN-кода
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
!pinNodes.isEmpty()) {
qDebug() << "[PayByPhone] PIN code screen detected after confirm, entering PIN...";
inputPinCode(deviceId, pinNodes);
QThread::msleep(3000);
}
// 9. Ждём результат: "Успешно" или "Не удалось"
for (int attempt = 0; attempt < 15; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
// Если PIN-код появился позже — вводим
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
!pinNodes.isEmpty()) {
qDebug() << "[PayByPhone] PIN code screen detected (retry), entering PIN...";
inputPinCode(deviceId, pinNodes);
QThread::msleep(3000);
continue;
}
if (!xmlScreenParser.findTextNode("Успешно").isEmpty()) {
qDebug() << "[PayByPhone] Payment successful!";
m_error.clear();
// Сохраняем транзакцию в БД
TransactionInfo tx;
tx.accountId = m_account.id;
tx.amount = -m_amount;
tx.phone = m_phone;
tx.bankName = "ozon";
tx.type = TransactionType::Phone;
tx.status = TransactionStatus::Complete;
tx.timestamp = DateUtils::getUtcNow();
tx.completeTime = QDateTime::currentDateTime().toTimeZone(QTimeZone("Europe/Moscow"));
tx.bankTime = tx.completeTime.toString("dd.MM.yyyy, HH:mm");
const int txId = TransactionDAO::insertTransaction(tx);
qDebug() << "[PayByPhone] Saved transaction to DB, id:" << txId;
// Ищем кнопку "Документы" и достаём ID операции
Node docBtn = xmlScreenParser.findButtonNode("Документы", false);
if (!docBtn.isEmpty() && txId != -1) {
qDebug() << "[PayByPhone] Tapping 'Документы'...";
AdbUtils::makeTap(deviceId, docBtn.x(), docBtn.y());
QThread::msleep(2000);
// Ждём появления кнопки "Поделиться"
bool docLoaded = false;
for (int da = 0; da < 20; ++da) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node shareBtn = xmlScreenParser.findNodeByContentDesc("Поделиться");
if (!shareBtn.isEmpty()) {
docLoaded = true;
break;
}
qDebug() << "[PayByPhone] Waiting for 'Поделиться', attempt" << da + 1 << "/ 20";
QThread::msleep(1500);
}
if (docLoaded) {
QThread::msleep(500);
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId);
if (!screenshot.isEmpty()) {
TransactionDAO::updateReceiptImage(txId, screenshot);
qDebug() << "[PayByPhone] Receipt saved to DB, txId:" << txId;
const QString ocrText = recognizeTextFromImage(screenshot);
if (!ocrText.isEmpty()) {
qDebug() << "[PayByPhone] OCR result:\n" << ocrText;
const QString externalId = extractTransactionId(ocrText);
if (!externalId.isEmpty()) {
TransactionDAO::updateBankTrExternalId(txId, externalId);
qDebug() << "[PayByPhone] Extracted transaction ID:" << externalId;
} else {
qWarning() << "[PayByPhone] Could not extract transaction ID from OCR text";
}
} else {
qWarning() << "[PayByPhone] OCR returned empty text";
}
} else {
qWarning() << "[PayByPhone] Screenshot is empty";
}
} else {
qWarning() << "[PayByPhone] 'Поделиться' not found, skipping receipt";
}
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
}
return;
}
if (!xmlScreenParser.findTextNode("Не удалось").isEmpty()) {
m_error = "Перевод не удался (экран 'Не удалось')";
qWarning() << "[PayByPhone]" << m_error;
return;
}
qDebug() << "[PayByPhone] Waiting for payment result, attempt" << attempt + 1 << "/ 15";
QThread::msleep(2000);
}
m_error = "Не дождались результата перевода";
qWarning() << "[PayByPhone]" << m_error;
}
} // namespace Ozon