- Handle fractional amounts during transaction parsing in mock server. - Add support for closing Google Play update banners in `Ozon` homepage handling logic.
432 lines
18 KiB
C++
432 lines
18 KiB
C++
#include "CommonScript.h"
|
||
|
||
#include <QJsonArray>
|
||
#include <QJsonObject>
|
||
#include <QRandomGenerator>
|
||
#include <QRegularExpression>
|
||
#include <QSettings>
|
||
#include <QThread>
|
||
|
||
#include "adb/AdbUtils.h"
|
||
#include "dao/DeviceDAO.h"
|
||
#include "dao/BankProfileDAO.h"
|
||
#include "net/NetworkService.h"
|
||
|
||
namespace Ozon {
|
||
|
||
CommonScript::CommonScript(QObject *parent)
|
||
: QObject(parent) {
|
||
}
|
||
|
||
CommonScript::~CommonScript() = default;
|
||
|
||
void CommonScript::start() {
|
||
doStart();
|
||
emit finished();
|
||
}
|
||
|
||
Node CommonScript::swipeToButton(
|
||
const QString &deviceId,
|
||
const QString &text, int &counter, const int width, const int height
|
||
) {
|
||
for (int i = 0; i < 10; ++i) {
|
||
if (Node button = xmlScreenParser.findButtonNode(text, true); button.x() > 0 && button.y() > 0) {
|
||
return button;
|
||
}
|
||
const int x = width / 2;
|
||
const int offset = height / 4;
|
||
AdbUtils::makeSwipe(deviceId, x, height - offset, x, offset);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++counter));
|
||
}
|
||
return {};
|
||
}
|
||
|
||
bool CommonScript::inputPinCode(const QString &deviceId, const QList<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 CommonScript::runAppAndGoToHomeScreen(
|
||
const QString &deviceId,
|
||
const QString &packageName,
|
||
const QString &pinCode,
|
||
const int width,
|
||
const int height
|
||
) {
|
||
// Разблокируем экран перед запуском
|
||
AdbUtils::unlockScreen(deviceId, width, height);
|
||
|
||
// Быстрый path: если приложение уже запущено — не перезапускаем сразу,
|
||
// а смотрим текущий экран. Home → готово. PIN → вводим и ждём Home.
|
||
// Только если не смогли — убиваем и рестартим.
|
||
if (const QString pid = AdbUtils::tryToGetRunningAppPid(deviceId, packageName); !pid.isEmpty()) {
|
||
qDebug() << "Process already running, pid: " << pid;
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
if (xmlScreenParser.isHomeScreen()) {
|
||
qDebug() << "[Ozon] Fast path: already on Home screen";
|
||
return true;
|
||
}
|
||
{
|
||
const QList<Node> fastPincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
||
if (fastPincode.size() == pinCode.length() && fastPincode.size() >= 4) {
|
||
qDebug() << "[Ozon] Fast path: PIN screen, entering PIN";
|
||
if (Node wake = xmlScreenParser.findTextNode("Введите ПИН"); !wake.isEmpty()) {
|
||
AdbUtils::makeTap(deviceId, wake.x(), wake.y());
|
||
}
|
||
inputPinCode(deviceId, fastPincode);
|
||
QThread::msleep(3000);
|
||
for (int i = 0; i < 10; ++i) {
|
||
xmlScreenParser.parseAndSaveXml(
|
||
AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
if (xmlScreenParser.isHomeScreen()) {
|
||
qDebug() << "[Ozon] Fast path: Home reached after PIN";
|
||
return true;
|
||
}
|
||
// Ошибка загрузки — тапаем "Обновить"
|
||
{
|
||
Node refreshBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Обновить"));
|
||
if (refreshBtn.x() > 0 && refreshBtn.y() > 0) {
|
||
qDebug() << "[Ozon] Fast path: page error after PIN, tapping 'Обновить'";
|
||
AdbUtils::makeTap(deviceId, refreshBtn.x(), refreshBtn.y());
|
||
QThread::msleep(5000);
|
||
}
|
||
}
|
||
QThread::msleep(1000);
|
||
}
|
||
}
|
||
}
|
||
qDebug() << "[Ozon] Fast path failed, restarting app";
|
||
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;
|
||
}
|
||
|
||
// Проверяем на ошибку загрузки WebView ("Обновить")
|
||
{
|
||
Node refreshBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Обновить"));
|
||
if (refreshBtn.x() > 0 && refreshBtn.y() > 0) {
|
||
qDebug() << "[runAppAndGoToHomeScreen] Page load error, tapping 'Обновить'";
|
||
AdbUtils::makeTap(deviceId, refreshBtn.x(), refreshBtn.y());
|
||
QThread::msleep(5000);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Проверяем на банеры перед пин-кодом
|
||
Node closeBannerNode = xmlScreenParser.tryToFindSecurityBanner();
|
||
if (!closeBannerNode.isEmpty()) {
|
||
AdbUtils::makeTap(deviceId, closeBannerNode.x(), closeBannerNode.y());
|
||
QThread::msleep(1000);
|
||
continue;
|
||
}
|
||
|
||
// Проверяем на баннер Google Play "Доступно обновление"
|
||
if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(width, height); !gpNode.isEmpty()) {
|
||
qDebug() << "[runAppAndGoToHomeScreen] Google Play update banner detected, closing...";
|
||
AdbUtils::makeTap(deviceId, gpNode.x(), gpNode.y());
|
||
QThread::msleep(1500);
|
||
continue;
|
||
}
|
||
|
||
// Проверяем на рекламный bottom sheet (ипотека, открыть счёт и т.п.)
|
||
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
|
||
qDebug() << "[runAppAndGoToHomeScreen] Ad bottom sheet detected, closing...";
|
||
AdbUtils::makeTap(deviceId, adNode.x(), adNode.y());
|
||
QThread::msleep(1000);
|
||
// Если touch_outside не помог — жмём назад
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
if (Node stillAd = xmlScreenParser.tryToFindAdBanner(); !stillAd.isEmpty()) {
|
||
qDebug() << "[runAppAndGoToHomeScreen] Ad still visible, pressing back...";
|
||
AdbUtils::goBack(deviceId);
|
||
QThread::msleep(1000);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Проверяем, что это экран ввода пин-кода
|
||
if (!findAndInputPincode) {
|
||
const QList<Node> pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
||
if (pincode.size() == pinCode.length() && pincode.size() >= 4) {
|
||
// чтобы пробудить экран
|
||
if (Node textPinCodeNode = xmlScreenParser.findTextNode("Введите ПИН"); !textPinCodeNode.isEmpty()) {
|
||
AdbUtils::makeTap(deviceId, textPinCodeNode.x(), textPinCodeNode.y());
|
||
}
|
||
inputPinCode(deviceId, pincode);
|
||
// После ввода пин-код нужно сделать задержку, пока подгрузка идет, чтобы снова не спарсить
|
||
findAndInputPincode = true;
|
||
QThread::msleep(3000);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
QThread::msleep(1000);
|
||
}
|
||
}
|
||
|
||
bool CommonScript::tryToCloseAllBannersOnHomePage(const QString &deviceId, const int width, const int height) {
|
||
bool closedAny = false;
|
||
for (int i = 0; i < 5; ++i) {
|
||
bool closedThis = false;
|
||
|
||
// Баннер Google Play "Доступно обновление" — появляется поверх Ozon при возврате на домашнюю
|
||
if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(width, height); !gpNode.isEmpty()) {
|
||
qDebug() << "[tryToCloseAllBannersOnHomePage] Google Play update banner detected, closing...";
|
||
AdbUtils::makeTap(deviceId, gpNode.x(), gpNode.y());
|
||
QThread::msleep(1500);
|
||
closedThis = true;
|
||
} else if (Node closeBannerOrDialogNode = xmlScreenParser.tryToFindBannerOrDialog(width, height);
|
||
closeBannerOrDialogNode.x() != -1 && closeBannerOrDialogNode.y() != -1) {
|
||
AdbUtils::makeTap(deviceId, closeBannerOrDialogNode.x(), closeBannerOrDialogNode.y());
|
||
QThread::msleep(500);
|
||
closedThis = true;
|
||
}
|
||
|
||
if (!closedThis) break;
|
||
closedAny = true;
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
}
|
||
return closedAny;
|
||
}
|
||
|
||
bool CommonScript::goToAllProducts(
|
||
const QString &deviceId,
|
||
const int width,
|
||
const int height
|
||
) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, m_counter));
|
||
if (tryToCloseAllBannersOnHomePage(deviceId, width, height)) {
|
||
QThread::msleep(500);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
}
|
||
|
||
// Ozon: домашний экран и есть "Финансы" — счета уже доступны
|
||
if (xmlScreenParser.isHomeScreen()) {
|
||
return true;
|
||
}
|
||
|
||
// Если по какой-то причине не на домашнем — пробуем тапнуть по вкладке "Финансы"
|
||
for (int i = 0; i < 3; ++i) {
|
||
if (Node financeTab = xmlScreenParser.findNodeByContentDesc("Финансы"); !financeTab.isEmpty()) {
|
||
AdbUtils::makeTap(deviceId, financeTab.x(), financeTab.y());
|
||
QThread::msleep(1500);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
if (xmlScreenParser.isHomeScreen()) {
|
||
return true;
|
||
}
|
||
}
|
||
QThread::msleep(1000);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
}
|
||
|
||
qDebug() << "goToAllProducts: not home screen";
|
||
return false;
|
||
}
|
||
|
||
bool CommonScript::findAndTapOnAccount(const QString &deviceId, const QString &accountNumbers) {
|
||
QList<QPair<MaterialInfo, 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 CommonScript::postAccountsData(const QList<QPair<MaterialInfo, Node> > &accounts) {
|
||
if (accounts.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
auto *thread = new QThread;
|
||
auto *worker = new NetworkService;
|
||
|
||
QJsonArray list;
|
||
for (const QPair<MaterialInfo, Node> &pair: accounts) {
|
||
QJsonObject obj;
|
||
MaterialInfo account = pair.first;
|
||
obj["appName"] = account.appCode;
|
||
obj["lastNumbers"] = account.lastNumbers;
|
||
obj["amount"] = account.amount;
|
||
obj["description"] = account.description;
|
||
obj["currency"] = account.currency;
|
||
obj["status"] = materialStatusToString(account.status);
|
||
|
||
list.append(obj);
|
||
}
|
||
// FIXME доставить десктоп ид из настроеек
|
||
worker->setDeviceId(accounts.first().first.deviceId);
|
||
worker->setPayload(QJsonDocument(list).toJson());
|
||
worker->moveToThread(thread);
|
||
connect(thread, &QThread::started, worker, &NetworkService::postAccountsData);
|
||
connect(worker, &NetworkService::finished, thread, &QThread::quit);
|
||
connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||
thread->start();
|
||
}
|
||
|
||
void CommonScript::postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction) {
|
||
auto *thread = new QThread;
|
||
auto *worker = new NetworkService;
|
||
|
||
QJsonObject obj;
|
||
obj["id"] = transaction.id;
|
||
obj["amount"] = transaction.amount;
|
||
obj["name"] = transaction.name;
|
||
obj["fee"] = transaction.fee;
|
||
obj["status"] = transactionStatusToString(transaction.status);
|
||
obj["type"] = transactionTypeToString(transaction.type);
|
||
obj["phone"] = transaction.phone;
|
||
obj["description"] = transaction.description;
|
||
obj["updatedDescription"] = transaction.updatedDescription;
|
||
obj["bankName"] = transaction.bankName;
|
||
obj["bankTime"] = transaction.bankTime;
|
||
obj["bankTransactionId"] = transaction.bankTrExternalId;
|
||
|
||
// FIXME доставить десктоп ид из настроеек
|
||
worker->setDeviceId(account.deviceId);
|
||
worker->addExtra("lastNumbers", account.lastNumbers);
|
||
worker->addExtra("transactionId", transaction.id);
|
||
worker->setPayload(QJsonDocument(obj).toJson());
|
||
worker->moveToThread(thread);
|
||
connect(thread, &QThread::started, worker, &NetworkService::insertTransactionData);
|
||
connect(worker, &NetworkService::finished, thread, &QThread::quit);
|
||
connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||
thread->start();
|
||
}
|
||
|
||
void CommonScript::updateTransactionData(
|
||
const MaterialInfo &account,
|
||
const int transactionId,
|
||
const TransactionStatus status,
|
||
const QString &oldDesc,
|
||
const QString &newDesc
|
||
) {
|
||
auto *thread = new QThread;
|
||
auto *worker = new NetworkService;
|
||
|
||
QJsonObject obj;
|
||
obj["id"] = transactionId;
|
||
obj["status"] = transactionStatusToString(status);
|
||
obj["description"] = newDesc;
|
||
obj["updatedDescription"] = oldDesc;
|
||
|
||
// FIXME доставить десктоп ид из настроеек
|
||
worker->setDeviceId(account.deviceId);
|
||
worker->addExtra("lastNumbers", account.lastNumbers);
|
||
worker->setPayload(QJsonDocument(obj).toJson());
|
||
worker->moveToThread(thread);
|
||
connect(thread, &QThread::started, worker, &NetworkService::updateTransactionData);
|
||
connect(worker, &NetworkService::finished, thread, &QThread::quit);
|
||
connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||
thread->start();
|
||
}
|
||
|
||
void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance) {
|
||
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
|
||
|
||
if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) {
|
||
qWarning() << "[Ozon] createBankProfileIfNeeded: skipping — fullName or phone is empty";
|
||
return;
|
||
}
|
||
|
||
const QMap<QString, int> currencyCodes = {
|
||
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
|
||
};
|
||
const int currencyCode = currencyCodes.value(app.currency.toUpper(), 643);
|
||
const QStringList parts = app.fullName.split(' ', Qt::SkipEmptyParts);
|
||
|
||
// Нормализуем телефон: оставляем только цифры, добавляем код страны и "+"
|
||
const QSettings settings("config.ini", QSettings::IniFormat);
|
||
const QString phoneCode = settings.value(appCode + "/phone_code").toString();
|
||
QString phone = app.phone;
|
||
phone.remove(QRegularExpression("[^0-9]"));
|
||
if (!phoneCode.isEmpty() && !phone.startsWith(phoneCode)) {
|
||
if (phone.startsWith('8') && phoneCode != "8") {
|
||
phone.replace(0, 1, phoneCode);
|
||
} else {
|
||
phone.prepend(phoneCode);
|
||
}
|
||
}
|
||
phone.prepend('+');
|
||
|
||
const DeviceInfo device = DeviceDAO::getDeviceById(deviceId);
|
||
|
||
QJsonObject profileBody;
|
||
profileBody["first_name"] = parts.value(0);
|
||
profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : QString("");
|
||
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
|
||
profileBody["bank_name"] = appCode;
|
||
profileBody["phone_number"] = phone;
|
||
const double balance = (parsedBalance >= 0.0) ? parsedBalance : app.amount;
|
||
profileBody["balance"] = QString::number(qRound64(balance * 100));
|
||
profileBody["state"] = (app.status == "active") ? "enabled" : "disabled";
|
||
profileBody["device_id"] = device.apiId;
|
||
profileBody["currency"] = currencyCode;
|
||
|
||
NetworkService ns;
|
||
ns.setDeviceId(deviceId);
|
||
ns.setPayload(QJsonDocument(profileBody).toJson());
|
||
|
||
if (app.bankProfileId.isEmpty()) {
|
||
ns.addExtra("app_id", app.id);
|
||
ns.postBankProfile();
|
||
qDebug() << "[Ozon] createBankProfileIfNeeded: postBankProfile done";
|
||
} else {
|
||
ns.addExtra("bank_profile_id", app.bankProfileId);
|
||
ns.patchBankProfile();
|
||
qDebug() << "[Ozon] createBankProfileIfNeeded: patchBankProfile done";
|
||
}
|
||
}
|
||
|
||
bool CommonScript::isAppRunning(const QString &deviceId, const QString &packageName) {
|
||
return !AdbUtils::tryToGetRunningAppPid(deviceId, packageName).isEmpty();
|
||
}
|
||
|
||
void CommonScript::stop() {
|
||
m_running = false;
|
||
}
|
||
|
||
} // namespace Ozon
|