1096 lines
50 KiB
C++
1096 lines
50 KiB
C++
#include "ScriptBridges.h"
|
||
|
||
#include <QBuffer>
|
||
#include <QByteArray>
|
||
#include <QDebug>
|
||
#include <QJsonDocument>
|
||
#include <QJsonObject>
|
||
#include <QDateTime>
|
||
#include <QRandomGenerator>
|
||
#include <QSettings>
|
||
#include <QString>
|
||
#include <QThread>
|
||
#include <QRegularExpression>
|
||
#include <QJsonArray>
|
||
|
||
#include "adb/AdbUtils.h"
|
||
#include "black/ScreenXmlParser.h"
|
||
#include "ozon/ScreenXmlParser.h"
|
||
#include "android/xml/Node.h"
|
||
#include "db/BankProfileInfo.h"
|
||
#include "db/DeviceInfo.h"
|
||
#include "db/MaterialInfo.h"
|
||
#include "db/TransactionInfo.h"
|
||
#include "dao/BankProfileDAO.h"
|
||
#include "dao/DeviceDAO.h"
|
||
#include "dao/MaterialDAO.h"
|
||
#include "dao/SettingsDAO.h"
|
||
#include "dao/TransactionDAO.h"
|
||
#include "dump/TaskDumpRecorder.h"
|
||
#include "net/NetworkService.h"
|
||
#include "ocr/OcrUtils.h"
|
||
|
||
namespace Scripting {
|
||
|
||
QVariantMap nodeToVariantMap(const Node &node) {
|
||
Node copy = node; // isEmpty() не const, нельзя дернуть на const-ссылке
|
||
QVariantMap m;
|
||
m["x1"] = copy.x1();
|
||
m["y1"] = copy.y1();
|
||
m["x2"] = copy.x2();
|
||
m["y2"] = copy.y2();
|
||
m["x"] = copy.x();
|
||
m["y"] = copy.y();
|
||
m["width"] = copy.width();
|
||
m["height"] = copy.height();
|
||
m["text"] = copy.text;
|
||
m["contentDesc"] = copy.contentDesc;
|
||
m["resourceId"] = copy.resourceId;
|
||
m["className"] = copy.className;
|
||
m["hint"] = copy.hint;
|
||
m["clickable"] = copy.clickable;
|
||
m["focusable"] = copy.focusable;
|
||
m["password"] = copy.password;
|
||
m["isEmpty"] = copy.isEmpty();
|
||
return m;
|
||
}
|
||
|
||
// ─── LogBridge ──────────────────────────────────────────────────────────────
|
||
|
||
void LogBridge::debug(const QString &message) { qDebug().noquote() << "[js:" + m_tag + "]" << message; }
|
||
void LogBridge::info (const QString &message) { qInfo().noquote() << "[js:" + m_tag + "]" << message; }
|
||
void LogBridge::warn (const QString &message) { qWarning().noquote() << "[js:" + m_tag + "]" << message; }
|
||
void LogBridge::error(const QString &message) { qCritical().noquote() << "[js:" + m_tag + "]" << message; }
|
||
|
||
// ─── TimerBridge ────────────────────────────────────────────────────────────
|
||
|
||
void TimerBridge::sleep(int ms) {
|
||
if (ms <= 0) return;
|
||
// Дробим длинный sleep на 100мс шаги, чтобы можно было прервать поток.
|
||
const int step = 100;
|
||
while (ms > 0) {
|
||
if (QThread *t = QThread::currentThread(); t && t->isInterruptionRequested()) return;
|
||
const int chunk = ms > step ? step : ms;
|
||
QThread::msleep(static_cast<unsigned long>(chunk));
|
||
ms -= chunk;
|
||
}
|
||
}
|
||
|
||
bool TimerBridge::isInterrupted() const {
|
||
QThread *t = QThread::currentThread();
|
||
return t && t->isInterruptionRequested();
|
||
}
|
||
|
||
// ─── AdbBridge ──────────────────────────────────────────────────────────────
|
||
|
||
QString AdbBridge::getScreenDumpAsXml(const QString &deviceId, int idx) {
|
||
return AdbUtils::getScreenDumpAsXml(deviceId, idx);
|
||
}
|
||
QString AdbBridge::getRawScreenDumpAsXml(const QString &deviceId) {
|
||
return AdbUtils::getRawScreenDumpAsXml(deviceId);
|
||
}
|
||
bool AdbBridge::makeTap(const QString &deviceId, int x, int y) {
|
||
return AdbUtils::makeTap(deviceId, x, y);
|
||
}
|
||
bool AdbBridge::makeSwipe(const QString &deviceId, int x1, int y1, int x2, int y2) {
|
||
return AdbUtils::makeSwipe(deviceId, x1, y1, x2, y2);
|
||
}
|
||
bool AdbBridge::makeSwipeWithDuration(const QString &deviceId, int x1, int y1, int x2, int y2, int durationMs) {
|
||
return AdbUtils::makeSwipe(deviceId, x1, y1, x2, y2, durationMs);
|
||
}
|
||
bool AdbBridge::tryToStartApplication(const QString &deviceId, const QString &packageName) {
|
||
return AdbUtils::tryToStartApplication(deviceId, packageName);
|
||
}
|
||
bool AdbBridge::tryToKillApplication(const QString &deviceId, const QString &packageName) {
|
||
return AdbUtils::tryToKillApplication(deviceId, packageName);
|
||
}
|
||
QString AdbBridge::tryToGetRunningAppPid(const QString &deviceId, const QString &packageName) {
|
||
return AdbUtils::tryToGetRunningAppPid(deviceId, packageName);
|
||
}
|
||
bool AdbBridge::takeScreenshot(const QString &deviceId, const QString &name) {
|
||
return AdbUtils::takeScreenshot(deviceId, name);
|
||
}
|
||
bool AdbBridge::inputText(const QString &deviceId, const QString &text) {
|
||
return AdbUtils::inputText(deviceId, text);
|
||
}
|
||
bool AdbBridge::inputAdbIMEText(const QString &deviceId, const QString &text) {
|
||
return AdbUtils::inputAdbIMEText(deviceId, text);
|
||
}
|
||
bool AdbBridge::enableAdbIMEKeyboard(const QString &deviceId) {
|
||
return AdbUtils::enableAdbIMEKeyboard(deviceId);
|
||
}
|
||
bool AdbBridge::disableAdbIMEKeyboard(const QString &deviceId) {
|
||
return AdbUtils::disableAdbIMEKeyboard(deviceId);
|
||
}
|
||
bool AdbBridge::hideKeyboard(const QString &deviceId) {
|
||
return AdbUtils::hideKeyboard(deviceId);
|
||
}
|
||
bool AdbBridge::goBack(const QString &deviceId) {
|
||
return AdbUtils::goBack(deviceId);
|
||
}
|
||
void AdbBridge::unlockScreen(const QString &deviceId, int screenWidth, int screenHeight) {
|
||
AdbUtils::unlockScreen(deviceId, screenWidth, screenHeight);
|
||
}
|
||
QString AdbBridge::getDeviceTimezone(const QString &deviceId) {
|
||
return AdbUtils::getDeviceTimezone(deviceId);
|
||
}
|
||
QString AdbBridge::captureScreenshotBase64(const QString &deviceId) {
|
||
const QByteArray bytes = AdbUtils::captureScreenshotBytes(deviceId);
|
||
return QString::fromLatin1(bytes.toBase64());
|
||
}
|
||
|
||
// ─── DaoBridge ──────────────────────────────────────────────────────────────
|
||
|
||
static QVariantMap deviceToMap(const DeviceInfo &d) {
|
||
QVariantMap m;
|
||
if (d.id.isEmpty()) return m; // пустая мапа = null
|
||
m["id"] = d.id;
|
||
m["adbSerial"] = d.adbSerial;
|
||
m["name"] = d.name;
|
||
m["screenWidth"] = d.screenWidth;
|
||
m["screenHeight"] = d.screenHeight;
|
||
m["apiId"] = d.apiId;
|
||
return m;
|
||
}
|
||
|
||
static QVariantMap bankProfileToMap(const BankProfileInfo &p) {
|
||
QVariantMap m;
|
||
if (p.id == -1) return m;
|
||
m["id"] = p.id;
|
||
m["appCode"] = p.code; // BankProfileInfo.code = код банка в config.ini ("black", "ozon"...)
|
||
m["name"] = p.name;
|
||
m["package"] = p.package;
|
||
m["pinCode"] = p.pinCode;
|
||
m["status"] = p.status;
|
||
m["amount"] = p.amount;
|
||
m["fullName"] = p.fullName;
|
||
m["phone"] = p.phone;
|
||
m["email"] = p.email;
|
||
m["currency"] = p.currency;
|
||
m["bankProfileId"] = p.bankProfileId;
|
||
return m;
|
||
}
|
||
|
||
static QVariantMap materialToMap(const MaterialInfo &a) {
|
||
QVariantMap m;
|
||
if (a.id == -1) return m;
|
||
m["id"] = a.id;
|
||
m["materialId"] = a.materialId;
|
||
m["appCode"] = a.appCode;
|
||
m["deviceId"] = a.deviceId;
|
||
m["lastNumbers"] = a.lastNumbers;
|
||
m["cardNumber"] = a.cardNumber;
|
||
m["accountNumber"] = a.accountNumber;
|
||
m["amount"] = a.amount;
|
||
m["currency"] = a.currency;
|
||
m["description"] = a.description;
|
||
m["status"] = materialStatusToString(a.status);
|
||
return m;
|
||
}
|
||
|
||
static MaterialInfo materialFromMap(const QVariantMap &m) {
|
||
MaterialInfo a;
|
||
a.id = m.value("id", -1).toInt();
|
||
a.materialId = m.value("materialId").toString();
|
||
a.appCode = m.value("appCode").toString();
|
||
a.deviceId = m.value("deviceId").toString();
|
||
a.lastNumbers = m.value("lastNumbers").toString();
|
||
a.cardNumber = m.value("cardNumber").toString();
|
||
a.accountNumber = m.value("accountNumber").toString();
|
||
a.amount = m.value("amount").toDouble();
|
||
a.currency = m.value("currency").toString();
|
||
a.description = m.value("description").toString();
|
||
const QString status = m.value("status").toString();
|
||
if (!status.isEmpty()) a.status = materialStatusFromString(status);
|
||
a.updateTime = QDateTime::currentDateTimeUtc();
|
||
return a;
|
||
}
|
||
|
||
QVariantMap DaoBridge::deviceGetById(const QString &id) {
|
||
return deviceToMap(DeviceDAO::getDeviceById(id));
|
||
}
|
||
QVariantMap DaoBridge::deviceFindByAdbSerial(const QString &serial) {
|
||
return deviceToMap(DeviceDAO::findByAdbSerial(serial));
|
||
}
|
||
QVariantMap DaoBridge::bankProfileGet(const QString &deviceId, const QString &appCode) {
|
||
return bankProfileToMap(BankProfileDAO::getApplication(deviceId, appCode));
|
||
}
|
||
bool DaoBridge::bankProfileUpdateAmount(int profileId, double amount) {
|
||
return BankProfileDAO::updateAmount(profileId, amount);
|
||
}
|
||
bool DaoBridge::bankProfileUpdateProfileInfo(int profileId, const QString &fullName,
|
||
const QString &phone, const QString &email) {
|
||
return BankProfileDAO::updateProfileInfo(profileId, fullName, phone, email);
|
||
}
|
||
bool DaoBridge::bankProfileUpdateComment(int profileId, const QString &comment) {
|
||
return BankProfileDAO::updateComment(profileId, comment);
|
||
}
|
||
QVariantList DaoBridge::materialGetAccounts(const QString &deviceId, const QString &appCode) {
|
||
QVariantList list;
|
||
for (const MaterialInfo &acc : MaterialDAO::getAccounts(deviceId, appCode)) {
|
||
list.append(materialToMap(acc));
|
||
}
|
||
return list;
|
||
}
|
||
QVariantMap DaoBridge::materialFindAppAccount(const QString &deviceId, const QString &appCode,
|
||
const QString &lastNumbers) {
|
||
return materialToMap(MaterialDAO::findAppAccount(deviceId, appCode, lastNumbers));
|
||
}
|
||
bool DaoBridge::materialUpsert(const QVariantMap &material) {
|
||
return MaterialDAO::upsertAccount(materialFromMap(material));
|
||
}
|
||
bool DaoBridge::materialUpdateCardNumber(int accountId, const QString &cardNumber) {
|
||
return MaterialDAO::updateCardNumber(accountId, cardNumber);
|
||
}
|
||
bool DaoBridge::materialUpdateAccountById(int id, const QString &status,
|
||
const QString &description, const QString ¤cy) {
|
||
return MaterialDAO::updateAccountById(id, status, description, currency);
|
||
}
|
||
QString DaoBridge::settingsGet(const QString &key) {
|
||
return SettingsDAO::get(key);
|
||
}
|
||
|
||
namespace {
|
||
TransactionInfo txFromMap(const QVariantMap &m) {
|
||
TransactionInfo tx;
|
||
tx.id = m.value("id", -1).toInt();
|
||
tx.accountId = m.value("accountId", -1).toInt();
|
||
tx.amount = m.value("amount").toDouble();
|
||
tx.fee = m.value("fee").toDouble();
|
||
tx.phone = m.value("phone").toString();
|
||
tx.name = m.value("name").toString();
|
||
tx.description = m.value("description").toString();
|
||
tx.updatedDescription = m.value("updatedDescription").toString();
|
||
tx.bankName = m.value("bankName").toString();
|
||
tx.bankTime = m.value("bankTime").toString();
|
||
tx.bankTrExternalId = m.value("bankTrExternalId").toString();
|
||
const QString typeS = m.value("type").toString();
|
||
if (!typeS.isEmpty()) tx.type = transactionTypeFromString(typeS);
|
||
const QString statusS = m.value("status").toString();
|
||
if (!statusS.isEmpty()) tx.status = transactionStatusFromString(statusS);
|
||
|
||
auto parseDt = [](const QVariant &v) -> QDateTime {
|
||
if (!v.isValid() || v.isNull()) return {};
|
||
if (v.canConvert<QDateTime>()) return v.toDateTime();
|
||
return QDateTime::fromString(v.toString(), Qt::ISODate);
|
||
};
|
||
tx.timestamp = parseDt(m.value("timestamp"));
|
||
tx.completeTime = parseDt(m.value("completeTime"));
|
||
tx.updateTime = parseDt(m.value("updateTime"));
|
||
if (!tx.timestamp.isValid()) tx.timestamp = QDateTime::currentDateTimeUtc();
|
||
return tx;
|
||
}
|
||
} // namespace
|
||
|
||
int DaoBridge::transactionInsert(const QVariantMap &tx) {
|
||
return TransactionDAO::insertTransaction(txFromMap(tx));
|
||
}
|
||
bool DaoBridge::transactionUpdateBankTrExternalId(int id, const QString &externalId) {
|
||
return TransactionDAO::updateBankTrExternalId(id, externalId);
|
||
}
|
||
|
||
// ─── DumpBridge ─────────────────────────────────────────────────────────────
|
||
|
||
void DumpBridge::beginScope(const QString &bank, const QString &taskKind, const QString &adbDeviceId,
|
||
const QVariantMap &meta) {
|
||
if (m_scope) {
|
||
qWarning() << "[Scripting] DumpBridge: beginScope called twice, ignoring";
|
||
return;
|
||
}
|
||
TaskDumpMeta tm;
|
||
tm.materialId = meta.value("materialId").toString();
|
||
tm.amount = meta.value("amount").toDouble();
|
||
tm.phone = meta.value("phone").toString();
|
||
tm.card = meta.value("card").toString();
|
||
m_errorPtr = new QString();
|
||
m_scope = new TaskDumpRecorder::Scope(bank, taskKind, adbDeviceId, tm, m_errorPtr);
|
||
}
|
||
|
||
void DumpBridge::endScope(const QString &errorMessage) {
|
||
if (!m_scope) return;
|
||
if (m_errorPtr) *m_errorPtr = errorMessage;
|
||
delete static_cast<TaskDumpRecorder::Scope *>(m_scope);
|
||
m_scope = nullptr;
|
||
delete m_errorPtr;
|
||
m_errorPtr = nullptr;
|
||
}
|
||
|
||
// ─── ScriptObjectBridge ────────────────────────────────────────────────────
|
||
|
||
void ScriptObjectBridge::emitTransactionParsed(const QString &jsonStr) {
|
||
if (!m_sink) {
|
||
qDebug() << "[Scripting] emitTransactionParsed called but no signalSink — dropping";
|
||
return;
|
||
}
|
||
QJsonParseError err;
|
||
const QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8(), &err);
|
||
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
|
||
qWarning() << "[Scripting] emitTransactionParsed: bad JSON" << err.errorString();
|
||
return;
|
||
}
|
||
// QMetaObject::invokeMethod умеет вызывать Qt-сигналы. Для thread-safety
|
||
// используем QueuedConnection — JS крутится в worker-потоке, EventHandler
|
||
// тоже там же, но queued тут не помешает.
|
||
QMetaObject::invokeMethod(m_sink, "transactionParsed", Qt::DirectConnection,
|
||
Q_ARG(QJsonObject, doc.object()));
|
||
}
|
||
|
||
// ─── OcrBridge ─────────────────────────────────────────────────────────────
|
||
|
||
QString OcrBridge::recognizeFromScreen(const QString &deviceId) {
|
||
const QByteArray png = AdbUtils::captureScreenshotBytes(deviceId);
|
||
if (png.isEmpty()) return {};
|
||
return OcrUtils::recognizeText(png);
|
||
}
|
||
|
||
QString OcrBridge::extractTransactionId(const QString &ocrText) {
|
||
return OcrUtils::extractTransactionId(ocrText);
|
||
}
|
||
|
||
// ─── BlackParserBridge ──────────────────────────────────────────────────────
|
||
|
||
BlackParserBridge::BlackParserBridge(QObject *parent)
|
||
: QObject(parent), m_parser(new Black::ScreenXmlParser(this)) {}
|
||
|
||
BlackParserBridge::~BlackParserBridge() = default;
|
||
|
||
void BlackParserBridge::parseAndSaveXml(const QString &xml) { m_parser->parseAndSaveXml(xml); }
|
||
bool BlackParserBridge::isHomeScreen() { return m_parser->isHomeScreen(); }
|
||
bool BlackParserBridge::isSideMenu() { return m_parser->isSideMenu(); }
|
||
bool BlackParserBridge::isPinCodeScreen() { return m_parser->isPinCodeScreen(); }
|
||
bool BlackParserBridge::isProfilePage() { return m_parser->isProfilePage(); }
|
||
bool BlackParserBridge::isCVUScreen() { return m_parser->isCVUScreen(); }
|
||
bool BlackParserBridge::isTransactionLoadingScreen() { return m_parser->isTransactionLoadingScreen(); }
|
||
bool BlackParserBridge::isTransactionListScreen() { return m_parser->isTransactionListScreen(); }
|
||
bool BlackParserBridge::isPayInputCardNumberScreen() { return m_parser->isPayInputCardNumberScreen(); }
|
||
bool BlackParserBridge::isPayTransferConfirmScreen() { return m_parser->isPayTransferConfirmScreen(); }
|
||
bool BlackParserBridge::isPayInputSumScreen() { return m_parser->isPayInputSumScreen(); }
|
||
bool BlackParserBridge::isPayConfirmScreen() { return m_parser->isPayConfirmScreen(); }
|
||
bool BlackParserBridge::isPaySuccessScreen() { return m_parser->isPaySuccessScreen(); }
|
||
|
||
QVariantMap BlackParserBridge::findHolaButton() { return nodeToVariantMap(m_parser->findHolaButton()); }
|
||
QVariantMap BlackParserBridge::findTextNode(const QString &text) { return nodeToVariantMap(m_parser->findTextNode(text)); }
|
||
QVariantMap BlackParserBridge::findNodeByContentDesc(const QString &desc) { return nodeToVariantMap(m_parser->findNodeByContentDesc(desc)); }
|
||
QVariantMap BlackParserBridge::findNodeByContentDescContains(const QString &desc) { return nodeToVariantMap(m_parser->findNodeByContentDesc(desc, true)); }
|
||
QVariantMap BlackParserBridge::findButtonNode(const QString &text, bool contains) { return nodeToVariantMap(m_parser->findButtonNode(text, contains)); }
|
||
QVariantMap BlackParserBridge::findFirstEditText() { return nodeToVariantMap(m_parser->findFirstEditText()); }
|
||
QVariantMap BlackParserBridge::findPinCodeEditText() { return nodeToVariantMap(m_parser->findPinCodeEditText()); }
|
||
QVariantMap BlackParserBridge::findVerMasAfterTuActividad() { return nodeToVariantMap(m_parser->findVerMasAfterTuActividad()); }
|
||
QVariantMap BlackParserBridge::findEditTextByHint(const QString &hint) { return nodeToVariantMap(m_parser->findEditTextByHint(hint)); }
|
||
|
||
QString BlackParserBridge::parseUserNameFromHomeScreen() { return m_parser->parseUserNameFromHomeScreen(); }
|
||
double BlackParserBridge::parseBalanceFromHomeScreen() { return m_parser->parseBalanceFromHomeScreen(); }
|
||
QString BlackParserBridge::parseProfileNombre() { return m_parser->parseProfileNombre(); }
|
||
QString BlackParserBridge::parseProfileApellido() { return m_parser->parseProfileApellido(); }
|
||
QString BlackParserBridge::parseProfileEmail() { return m_parser->parseProfileEmail(); }
|
||
QString BlackParserBridge::parseProfilePhone() { return m_parser->parseProfilePhone(); }
|
||
QString BlackParserBridge::parseCVUNumber() { return m_parser->parseCVUNumber(); }
|
||
QString BlackParserBridge::parseRecipientNameFromConfirm(){ return m_parser->parseRecipientNameFromConfirm(); }
|
||
QString BlackParserBridge::parsePaySuccessDateTime() { return m_parser->parsePaySuccessDateTime(); }
|
||
QString BlackParserBridge::parsePaySuccessCoelsaId() { return m_parser->parsePaySuccessCoelsaId(); }
|
||
|
||
// ─── BlackHelpersBridge ─────────────────────────────────────────────────────
|
||
|
||
// Локальная копия логики Black::CommonScript::runAppAndGoToHomeScreen — нужна
|
||
// чтобы JS мог вызвать готовый высокоуровневый flow, не переписывая login заново.
|
||
bool BlackHelpersBridge::runAppAndGoToHomeScreen(const QString &deviceId, const QString &packageName,
|
||
const QString &pinCode, int width, int height) {
|
||
Black::ScreenXmlParser *parser = m_parserBridge->parser();
|
||
|
||
AdbUtils::unlockScreen(deviceId, width, height);
|
||
|
||
if (const QString pid = AdbUtils::tryToGetRunningAppPid(deviceId, packageName); !pid.isEmpty()) {
|
||
qDebug() << "[js:black/helpers] 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() << "[js:black/helpers] Process has not started";
|
||
return false;
|
||
}
|
||
|
||
QThread::msleep(4500);
|
||
int counter = 0;
|
||
bool pinInjected = false;
|
||
|
||
int xmlCounter = 0;
|
||
while (true) {
|
||
if (QThread *t = QThread::currentThread(); t && t->isInterruptionRequested()) return false;
|
||
counter++;
|
||
if (counter > 10) {
|
||
AdbUtils::takeScreenshot(deviceId, "too_many_attempts");
|
||
return false;
|
||
}
|
||
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++xmlCounter));
|
||
|
||
if (parser->isHomeScreen()) return true;
|
||
|
||
if (!pinInjected && parser->isPinCodeScreen()) {
|
||
// Inline-копия inputPinCode (тоже из CommonScript)
|
||
Node editText = parser->findPinCodeEditText();
|
||
if (!editText.isEmpty()) {
|
||
AdbUtils::makeTap(deviceId, editText.x(), editText.y());
|
||
QThread::msleep(500);
|
||
AdbUtils::inputText(deviceId, pinCode);
|
||
QThread::msleep(500);
|
||
} else {
|
||
qWarning() << "[js:black/helpers] PIN EditText not found";
|
||
}
|
||
pinInjected = true;
|
||
QThread::msleep(3000);
|
||
continue;
|
||
}
|
||
QThread::msleep(1000);
|
||
}
|
||
}
|
||
|
||
void BlackHelpersBridge::postAccountsData(const QVariantList &accounts) {
|
||
if (accounts.isEmpty()) return;
|
||
auto *thread = new QThread;
|
||
auto *worker = new NetworkService;
|
||
|
||
QJsonArray list;
|
||
QString deviceId;
|
||
for (const QVariant &v : accounts) {
|
||
const QVariantMap acc = v.toMap();
|
||
QJsonObject obj;
|
||
obj["appName"] = acc.value("appCode").toString();
|
||
obj["lastNumbers"] = acc.value("lastNumbers").toString();
|
||
obj["amount"] = acc.value("amount").toDouble();
|
||
obj["description"] = acc.value("description").toString();
|
||
obj["currency"] = acc.value("currency").toString();
|
||
obj["status"] = acc.value("status").toString();
|
||
list.append(obj);
|
||
if (deviceId.isEmpty()) deviceId = acc.value("deviceId").toString();
|
||
}
|
||
|
||
worker->setDeviceId(deviceId);
|
||
worker->setPayload(QJsonDocument(list).toJson());
|
||
worker->moveToThread(thread);
|
||
QObject::connect(thread, &QThread::started, worker, &NetworkService::postAccountsData);
|
||
QObject::connect(worker, &NetworkService::finished, thread, &QThread::quit);
|
||
QObject::connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
|
||
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||
thread->start();
|
||
}
|
||
|
||
void BlackHelpersBridge::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() << "[js:black/helpers] 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(), 840);
|
||
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();
|
||
} else {
|
||
ns.addExtra("bank_profile_id", app.bankProfileId);
|
||
ns.patchBankProfile();
|
||
}
|
||
}
|
||
|
||
// ─── OzonParserBridge ──────────────────────────────────────────────────────
|
||
|
||
OzonParserBridge::OzonParserBridge(QObject *parent)
|
||
: QObject(parent), m_parser(new Ozon::ScreenXmlParser(this)) {}
|
||
|
||
OzonParserBridge::~OzonParserBridge() = default;
|
||
|
||
void OzonParserBridge::parseAndSaveXml(const QString &xml) { m_parser->parseAndSaveXml(xml); }
|
||
bool OzonParserBridge::isHomeScreen() { return m_parser->isHomeScreen(); }
|
||
bool OzonParserBridge::isProfileScreen() { return m_parser->isProfileScreen(); }
|
||
|
||
QVariantMap OzonParserBridge::tryToFindAdBanner() { return nodeToVariantMap(m_parser->tryToFindAdBanner()); }
|
||
QVariantMap OzonParserBridge::findUserNameOnHomeScreen(int screenHeight){ return nodeToVariantMap(m_parser->findUserNameOnHomeScreen(screenHeight)); }
|
||
QVariantMap OzonParserBridge::findCardButtonByLastNumbers(const QString &ln) { return nodeToVariantMap(m_parser->findCardButtonByLastNumbers(ln)); }
|
||
QVariantMap OzonParserBridge::findButtonNode(const QString &text, bool contains) { return nodeToVariantMap(m_parser->findButtonNode(text, contains)); }
|
||
QVariantMap OzonParserBridge::findTextNode(const QString &text) { return nodeToVariantMap(m_parser->findTextNode(text)); }
|
||
|
||
QVariantMap OzonParserBridge::tryToFindGooglePlayBanner(int w, int h) { return nodeToVariantMap(m_parser->tryToFindGooglePlayBanner(w, h)); }
|
||
QVariantMap OzonParserBridge::findFirstEditText() { return nodeToVariantMap(m_parser->findFirstEditText()); }
|
||
QVariantMap OzonParserBridge::findEditTextByHint(const QString &h) { return nodeToVariantMap(m_parser->findEditTextByHint(h)); }
|
||
QVariantMap OzonParserBridge::findPanCardNumberInput() { return nodeToVariantMap(m_parser->findPanCardNumberInput()); }
|
||
QVariantMap OzonParserBridge::findMainAccountButton() { return nodeToVariantMap(m_parser->findMainAccountButton()); }
|
||
QVariantMap OzonParserBridge::findNodeByContentDesc(const QString &cd) { return nodeToVariantMap(m_parser->findNodeByContentDesc(cd)); }
|
||
|
||
QVariantList OzonParserBridge::findAllNodes() {
|
||
QVariantList list;
|
||
for (const Node &n : m_parser->findAllNodes()) list.append(nodeToVariantMap(n));
|
||
return list;
|
||
}
|
||
QVariantList OzonParserBridge::findAllButtons() {
|
||
QVariantList list;
|
||
for (const Node &n : m_parser->findAllButtons()) list.append(nodeToVariantMap(n));
|
||
return list;
|
||
}
|
||
QVariantList OzonParserBridge::tryToFindPinCode(const QString &pin, const QString &title) {
|
||
QVariantList list;
|
||
for (const Node &n : m_parser->tryToFindPinCode(pin, title)) list.append(nodeToVariantMap(n));
|
||
return list;
|
||
}
|
||
|
||
namespace {
|
||
|
||
// Конверсия TransactionInfo → JS-объект. Используется парсер-методами,
|
||
// возвращающими спарсенные транзакции из XML.
|
||
QVariantMap txInfoToMap(const TransactionInfo &tx) {
|
||
QVariantMap m;
|
||
m["id"] = tx.id;
|
||
m["accountId"] = tx.accountId;
|
||
m["amount"] = tx.amount;
|
||
m["fee"] = tx.fee;
|
||
m["status"] = transactionStatusToString(tx.status);
|
||
m["type"] = transactionTypeToString(tx.type);
|
||
m["phone"] = tx.phone;
|
||
m["name"] = tx.name;
|
||
m["description"] = tx.description;
|
||
m["updatedDescription"] = tx.updatedDescription;
|
||
m["bankName"] = tx.bankName;
|
||
m["bankTime"] = tx.bankTime;
|
||
m["bankTrExternalId"] = tx.bankTrExternalId;
|
||
if (tx.timestamp.isValid()) m["timestamp"] = tx.timestamp.toString(Qt::ISODateWithMs);
|
||
if (tx.completeTime.isValid()) m["completeTime"] = tx.completeTime.toString(Qt::ISODateWithMs);
|
||
if (tx.updateTime.isValid()) m["updateTime"] = tx.updateTime.toString(Qt::ISODateWithMs);
|
||
return m;
|
||
}
|
||
|
||
} // namespace
|
||
|
||
bool OzonParserBridge::isTransactionListScreen() { return m_parser->isTransactionListScreen(); }
|
||
bool OzonParserBridge::hasCollapsedTransactionsBelow(int sh) { return m_parser->hasCollapsedTransactionsBelow(sh); }
|
||
|
||
QVariantList OzonParserBridge::findTransactionButtons(int maxCount) {
|
||
QVariantList list;
|
||
for (const Node &n : m_parser->findTransactionButtons(maxCount)) list.append(nodeToVariantMap(n));
|
||
return list;
|
||
}
|
||
QVariantList OzonParserBridge::getLast2DaysTransactions() {
|
||
QVariantList list;
|
||
for (const Node &n : m_parser->getLast2DaysTransactions()) list.append(nodeToVariantMap(n));
|
||
return list;
|
||
}
|
||
QVariantList OzonParserBridge::parseTodayAndYesterdayHistory() {
|
||
QVariantList list;
|
||
for (const TransactionInfo &t : m_parser->parseTodayAndYesterdayHistory()) list.append(txInfoToMap(t));
|
||
return list;
|
||
}
|
||
QVariantList OzonParserBridge::parseTodayAndYesterdayReportHistory() {
|
||
QVariantList list;
|
||
for (const TransactionInfo &t : m_parser->parseTodayAndYesterdayReportHistory()) list.append(txInfoToMap(t));
|
||
return list;
|
||
}
|
||
QVariantMap OzonParserBridge::parseTransactionInfo(int w, int h) { return txInfoToMap(m_parser->parseTransactionInfo(w, h)); }
|
||
QVariantMap OzonParserBridge::parseTransactionInfoHistory(int w, int h) { return txInfoToMap(m_parser->parseTransactionInfoHistory(w, h)); }
|
||
QVariantMap OzonParserBridge::parseTransactionDetail() { return txInfoToMap(m_parser->parseTransactionDetail()); }
|
||
QVariantMap OzonParserBridge::parseTransactionButtonText(const QString &text) { return txInfoToMap(m_parser->parseTransactionButtonText(text)); }
|
||
|
||
QVariantList OzonParserBridge::parseCardsOnHomeScreen() {
|
||
QVariantList list;
|
||
for (const MaterialInfo &c : m_parser->parseCardsOnHomeScreen()) {
|
||
QVariantMap m;
|
||
m["lastNumbers"] = c.lastNumbers;
|
||
m["description"] = c.description;
|
||
m["amount"] = c.amount;
|
||
m["currency"] = c.currency;
|
||
list.append(m);
|
||
}
|
||
return list;
|
||
}
|
||
|
||
QString OzonParserBridge::parseProfileFullName() { return m_parser->parseProfileFullName(); }
|
||
QString OzonParserBridge::parseProfilePhone() { return m_parser->parseProfilePhone(); }
|
||
QString OzonParserBridge::findFullCardNumber() { return m_parser->findFullCardNumber(); }
|
||
|
||
QVariantList OzonParserBridge::parseAccountsInfo() {
|
||
QVariantList list;
|
||
for (const auto &pair : m_parser->parseAccountsInfo()) {
|
||
QVariantMap m;
|
||
m["lastNumbers"] = pair.first.lastNumbers;
|
||
m["description"] = pair.first.description;
|
||
m["amount"] = pair.first.amount;
|
||
m["currency"] = pair.first.currency;
|
||
QVariantMap entry;
|
||
entry["material"] = m;
|
||
entry["node"] = nodeToVariantMap(pair.second);
|
||
list.append(entry);
|
||
}
|
||
return list;
|
||
}
|
||
|
||
// ─── OzonHelpersBridge ─────────────────────────────────────────────────────
|
||
|
||
namespace {
|
||
|
||
// Локальная копия Ozon::CommonScript::inputPinCode — нужна целиком в helper'е,
|
||
// чтобы не тянуть CommonScript-инстанс ради одной static-функции.
|
||
bool ozonInputPinCode(const QString &deviceId, const QList<Node> &pincode) {
|
||
for (const Node &node : pincode) {
|
||
if (!AdbUtils::makeTap(deviceId, node.x(), node.y())) {
|
||
AdbUtils::makeTap(deviceId, node.x(), node.y());
|
||
}
|
||
QThread::msleep(QRandomGenerator::global()->bounded(100, 901));
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool interrupted() {
|
||
QThread *t = QThread::currentThread();
|
||
return t && t->isInterruptionRequested();
|
||
}
|
||
|
||
} // namespace
|
||
|
||
// Полная копия Ozon::CommonScript::runAppAndGoToHomeScreen — fast path + цикл с
|
||
// обработкой баннеров и пин-кода. Использует ТОТ ЖЕ парсер, что и JS-скрипт.
|
||
bool OzonHelpersBridge::runAppAndGoToHomeScreen(const QString &deviceId, const QString &packageName,
|
||
const QString &pinCode, int width, int height) {
|
||
Ozon::ScreenXmlParser *parser = m_parserBridge->parser();
|
||
int xmlCounter = 0;
|
||
|
||
AdbUtils::unlockScreen(deviceId, width, height);
|
||
|
||
// Fast path: если приложение уже запущено
|
||
if (const QString pid = AdbUtils::tryToGetRunningAppPid(deviceId, packageName); !pid.isEmpty()) {
|
||
qDebug() << "[js:ozon/helpers] Process already running, pid:" << pid;
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++xmlCounter));
|
||
if (parser->isHomeScreen()) {
|
||
qDebug() << "[js:ozon/helpers] Fast path: already on Home screen";
|
||
return true;
|
||
}
|
||
const QList<Node> fastPincode = parser->tryToFindPinCode(pinCode, QStringLiteral("Введите ПИН"));
|
||
if (fastPincode.size() == pinCode.length() && fastPincode.size() >= 4) {
|
||
qDebug() << "[js:ozon/helpers] Fast path: PIN screen, entering PIN";
|
||
if (Node wake = parser->findTextNode(QStringLiteral("Введите ПИН")); !wake.isEmpty()) {
|
||
AdbUtils::makeTap(deviceId, wake.x(), wake.y());
|
||
}
|
||
ozonInputPinCode(deviceId, fastPincode);
|
||
QThread::msleep(3000);
|
||
for (int i = 0; i < 10; ++i) {
|
||
if (interrupted()) return false;
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++xmlCounter));
|
||
if (parser->isHomeScreen()) {
|
||
qDebug() << "[js:ozon/helpers] Fast path: Home reached after PIN";
|
||
return true;
|
||
}
|
||
if (Node refreshBtn = parser->findNodeByContentDesc(QStringLiteral("Обновить"));
|
||
refreshBtn.x() > 0 && refreshBtn.y() > 0) {
|
||
qDebug() << "[js:ozon/helpers] Fast path: page error after PIN, tapping 'Обновить'";
|
||
AdbUtils::makeTap(deviceId, refreshBtn.x(), refreshBtn.y());
|
||
QThread::msleep(5000);
|
||
}
|
||
QThread::msleep(1000);
|
||
}
|
||
}
|
||
qDebug() << "[js:ozon/helpers] Fast path failed, restarting app";
|
||
AdbUtils::tryToKillApplication(deviceId, packageName);
|
||
}
|
||
|
||
if (!AdbUtils::tryToStartApplication(deviceId, packageName)) {
|
||
AdbUtils::takeScreenshot(deviceId, "run_app_error");
|
||
qWarning() << "[js:ozon/helpers] Process has not started";
|
||
return false;
|
||
}
|
||
|
||
QThread::msleep(4500);
|
||
int counter = 0;
|
||
bool pinInjected = false;
|
||
|
||
while (true) {
|
||
if (interrupted()) return false;
|
||
if (++counter > 10) {
|
||
AdbUtils::takeScreenshot(deviceId, "too_many_attempts");
|
||
return false;
|
||
}
|
||
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++xmlCounter));
|
||
|
||
if (parser->isHomeScreen()) return true;
|
||
|
||
if (Node bannerOrDialog = parser->tryToFindBannerOrDialog(width, height);
|
||
bannerOrDialog.x() != -1 && bannerOrDialog.y() != -1) {
|
||
AdbUtils::makeTap(deviceId, bannerOrDialog.x(), bannerOrDialog.y());
|
||
QThread::msleep(1000);
|
||
continue;
|
||
}
|
||
|
||
if (Node refreshBtn = parser->findNodeByContentDesc(QStringLiteral("Обновить"));
|
||
refreshBtn.x() > 0 && refreshBtn.y() > 0) {
|
||
qDebug() << "[js:ozon/helpers] Page load error, tapping 'Обновить'";
|
||
AdbUtils::makeTap(deviceId, refreshBtn.x(), refreshBtn.y());
|
||
QThread::msleep(5000);
|
||
continue;
|
||
}
|
||
|
||
if (Node securityBanner = parser->tryToFindSecurityBanner(); !securityBanner.isEmpty()) {
|
||
AdbUtils::makeTap(deviceId, securityBanner.x(), securityBanner.y());
|
||
QThread::msleep(1000);
|
||
continue;
|
||
}
|
||
|
||
if (Node gpNode = parser->tryToFindGooglePlayBanner(width, height); !gpNode.isEmpty()) {
|
||
qDebug() << "[js:ozon/helpers] Google Play update banner detected, closing...";
|
||
AdbUtils::makeTap(deviceId, gpNode.x(), gpNode.y());
|
||
QThread::msleep(1500);
|
||
continue;
|
||
}
|
||
|
||
if (Node adNode = parser->tryToFindAdBanner(); !adNode.isEmpty()) {
|
||
qDebug() << "[js:ozon/helpers] Ad bottom sheet detected, closing...";
|
||
AdbUtils::makeTap(deviceId, adNode.x(), adNode.y());
|
||
QThread::msleep(1000);
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++xmlCounter));
|
||
if (Node stillAd = parser->tryToFindAdBanner(); !stillAd.isEmpty()) {
|
||
qDebug() << "[js:ozon/helpers] Ad still visible, pressing back...";
|
||
AdbUtils::goBack(deviceId);
|
||
QThread::msleep(1000);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (!pinInjected) {
|
||
const QList<Node> pincode = parser->tryToFindPinCode(pinCode, QStringLiteral("Введите ПИН"));
|
||
if (pincode.size() == pinCode.length() && pincode.size() >= 4) {
|
||
if (Node wake = parser->findTextNode(QStringLiteral("Введите ПИН")); !wake.isEmpty()) {
|
||
AdbUtils::makeTap(deviceId, wake.x(), wake.y());
|
||
}
|
||
ozonInputPinCode(deviceId, pincode);
|
||
pinInjected = true;
|
||
QThread::msleep(3000);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
QThread::msleep(1000);
|
||
}
|
||
}
|
||
|
||
bool OzonHelpersBridge::tryToCloseAllBannersOnHomePage(const QString &deviceId, int width, int height) {
|
||
Ozon::ScreenXmlParser *parser = m_parserBridge->parser();
|
||
int xmlCounter = 0;
|
||
bool closedAny = false;
|
||
|
||
for (int i = 0; i < 5; ++i) {
|
||
if (interrupted()) return closedAny;
|
||
bool closedThis = false;
|
||
|
||
if (Node gpNode = parser->tryToFindGooglePlayBanner(width, height); !gpNode.isEmpty()) {
|
||
qDebug() << "[js:ozon/helpers] GP banner detected, closing";
|
||
AdbUtils::makeTap(deviceId, gpNode.x(), gpNode.y());
|
||
QThread::msleep(1500);
|
||
closedThis = true;
|
||
} else if (Node adNode = parser->tryToFindAdBanner(); !adNode.isEmpty()) {
|
||
qDebug() << "[js:ozon/helpers] Ad bottom sheet detected, closing";
|
||
AdbUtils::makeTap(deviceId, adNode.x(), adNode.y());
|
||
QThread::msleep(1000);
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++xmlCounter));
|
||
if (Node stillAd = parser->tryToFindAdBanner(); !stillAd.isEmpty()) {
|
||
AdbUtils::goBack(deviceId);
|
||
QThread::msleep(1000);
|
||
}
|
||
closedAny = true;
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++xmlCounter));
|
||
continue;
|
||
} else if (Node bod = parser->tryToFindBannerOrDialog(width, height);
|
||
bod.x() != -1 && bod.y() != -1) {
|
||
AdbUtils::makeTap(deviceId, bod.x(), bod.y());
|
||
QThread::msleep(500);
|
||
closedThis = true;
|
||
}
|
||
|
||
if (!closedThis) break;
|
||
closedAny = true;
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++xmlCounter));
|
||
}
|
||
return closedAny;
|
||
}
|
||
|
||
bool OzonHelpersBridge::closeGooglePlayBannerIfVisible(const QString &deviceId, int width, int height) {
|
||
Ozon::ScreenXmlParser *parser = m_parserBridge->parser();
|
||
int xmlCounter = 0;
|
||
|
||
if (Node denyBtn = parser->tryToFindSystemPermissionDenyButton(); !denyBtn.isEmpty()) {
|
||
qDebug() << "[js:ozon/helpers] System permission dialog detected, tapping 'ЗАПРЕТИТЬ'";
|
||
AdbUtils::makeTap(deviceId, denyBtn.x(), denyBtn.y());
|
||
QThread::msleep(1500);
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++xmlCounter));
|
||
return true;
|
||
}
|
||
|
||
Node gpNode = parser->tryToFindGooglePlayBanner(width, height);
|
||
if (gpNode.isEmpty()) return false;
|
||
qDebug() << "[js:ozon/helpers] GP banner detected mid-task, closing";
|
||
AdbUtils::makeTap(deviceId, gpNode.x(), gpNode.y());
|
||
QThread::msleep(1500);
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++xmlCounter));
|
||
return true;
|
||
}
|
||
|
||
void OzonHelpersBridge::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() << "[js:ozon/helpers] 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();
|
||
} else {
|
||
ns.addExtra("bank_profile_id", app.bankProfileId);
|
||
ns.patchBankProfile();
|
||
}
|
||
}
|
||
|
||
// Порт Ozon::CommonScript::goToAllProducts — для Ozon "Финансы" tab.
|
||
bool OzonHelpersBridge::goToAllProducts(const QString &deviceId, int width, int height) {
|
||
Ozon::ScreenXmlParser *parser = m_parserBridge->parser();
|
||
int xmlCounter = 0;
|
||
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, xmlCounter));
|
||
if (tryToCloseAllBannersOnHomePage(deviceId, width, height)) {
|
||
QThread::msleep(500);
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++xmlCounter));
|
||
}
|
||
|
||
if (parser->isHomeScreen()) return true;
|
||
|
||
for (int i = 0; i < 3; ++i) {
|
||
if (interrupted()) return false;
|
||
if (Node financeTab = parser->findNodeByContentDesc(QStringLiteral("Финансы"));
|
||
!financeTab.isEmpty()) {
|
||
AdbUtils::makeTap(deviceId, financeTab.x(), financeTab.y());
|
||
QThread::msleep(1500);
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++xmlCounter));
|
||
if (parser->isHomeScreen()) return true;
|
||
}
|
||
QThread::msleep(1000);
|
||
parser->parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++xmlCounter));
|
||
}
|
||
qDebug() << "[js:ozon/helpers] goToAllProducts: not home screen";
|
||
return false;
|
||
}
|
||
|
||
void OzonHelpersBridge::createDeviceIfNeeded(const QVariantMap &deviceObj) {
|
||
const QString apiId = deviceObj.value("apiId").toString();
|
||
if (!apiId.isEmpty()) return;
|
||
|
||
QJsonObject body;
|
||
body["desktop_id"] = SettingsDAO::get("desktop_id");
|
||
body["status"] = "ONLINE";
|
||
body["name"] = deviceObj.value("name").toString();
|
||
body["screen_width"] = deviceObj.value("screenWidth").toInt();
|
||
body["screen_height"]= deviceObj.value("screenHeight").toInt();
|
||
body["battery"] = deviceObj.value("battery").toInt();
|
||
|
||
NetworkService ns;
|
||
ns.setDeviceId(deviceObj.value("adbSerial").toString());
|
||
ns.setPayload(QJsonDocument(body).toJson());
|
||
ns.createDevice();
|
||
}
|
||
|
||
void OzonHelpersBridge::postMaterial(const QVariantMap &material, const QString &bankProfileId,
|
||
const QString &fullName) {
|
||
if (bankProfileId.isEmpty()) return;
|
||
if (material.value("cardNumber").toString().isEmpty()) return;
|
||
if (!material.value("materialId").toString().isEmpty()) return;
|
||
|
||
const QMap<QString, int> currencyCodes = {
|
||
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
|
||
};
|
||
QJsonObject body;
|
||
body["bank_profile_id"] = bankProfileId;
|
||
body["card_number"] = material.value("cardNumber").toString();
|
||
body["account_number"] = material.value("accountNumber").toString();
|
||
body["name"] = fullName;
|
||
body["state"] = material.value("status").toString() == "active" ? "enabled" : "disabled";
|
||
body["currency"] = currencyCodes.value(material.value("currency").toString().toUpper(), 643);
|
||
|
||
NetworkService ns;
|
||
ns.setDeviceId(material.value("deviceId").toString());
|
||
ns.setPayload(QJsonDocument(body).toJson());
|
||
ns.addExtra("account_id", material.value("id").toInt());
|
||
ns.postMaterial();
|
||
}
|
||
|
||
bool OzonHelpersBridge::isAppRunning(const QString &deviceId, const QString &packageName) {
|
||
return !AdbUtils::tryToGetRunningAppPid(deviceId, packageName).isEmpty();
|
||
}
|
||
|
||
bool OzonHelpersBridge::inputPinCode(const QString &deviceId, const QVariantList &pincodeNodes) {
|
||
// Та же логика, что и Ozon::CommonScript::inputPinCode — random delay 100-900мс,
|
||
// повторный тап при первом fail.
|
||
for (const QVariant &v : pincodeNodes) {
|
||
const QVariantMap n = v.toMap();
|
||
const int x = n.value("x").toInt();
|
||
const int y = n.value("y").toInt();
|
||
if (!AdbUtils::makeTap(deviceId, x, y)) {
|
||
AdbUtils::makeTap(deviceId, x, y);
|
||
}
|
||
QThread::msleep(QRandomGenerator::global()->bounded(100, 901));
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// Та же логика, что FreezeDetector::isUsefulDump в Ozon::CommonScript.
|
||
// Полезный дамп должен содержать Button с непустым text ИЛИ EditText.
|
||
bool OzonHelpersBridge::isUsefulDump(const QString &xml) {
|
||
if (xml.isEmpty()) return false;
|
||
if (!xml.contains(QStringLiteral("class=\"android.widget.Button\""))) return false;
|
||
static const QRegularExpression usefulRe(
|
||
R"(class="android\.widget\.Button"[^/]*text="[^"]+"|text="[^"]+"[^/]*class="android\.widget\.Button"|class="android\.widget\.EditText")"
|
||
);
|
||
return usefulRe.match(xml).hasMatch();
|
||
}
|
||
|
||
void OzonHelpersBridge::saveTransactionReceipt(const QString &deviceId, int transactionId) {
|
||
if (transactionId < 0) return;
|
||
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId);
|
||
if (screenshot.isEmpty()) return;
|
||
if (!TransactionDAO::updateReceiptImage(transactionId, screenshot)) {
|
||
qWarning() << "[js:ozon/helpers] updateReceiptImage failed for txId" << transactionId;
|
||
}
|
||
}
|
||
|
||
void OzonHelpersBridge::postNewTransaction(const QVariantMap &accountMap, const QVariantMap &txMap) {
|
||
// Зеркало Ozon::CommonScript::postNewTransactionData. Шлёт через
|
||
// NetworkService::insertTransactionData в worker-потоке.
|
||
auto *thread = new QThread;
|
||
auto *worker = new NetworkService;
|
||
|
||
QJsonObject obj;
|
||
obj["id"] = txMap.value("id").toInt();
|
||
obj["amount"] = txMap.value("amount").toDouble();
|
||
obj["name"] = txMap.value("name").toString();
|
||
obj["fee"] = txMap.value("fee").toDouble();
|
||
obj["status"] = txMap.value("status").toString();
|
||
obj["type"] = txMap.value("type").toString();
|
||
obj["phone"] = txMap.value("phone").toString();
|
||
obj["description"] = txMap.value("description").toString();
|
||
obj["updatedDescription"] = txMap.value("updatedDescription").toString();
|
||
obj["bankName"] = txMap.value("bankName").toString();
|
||
obj["bankTime"] = txMap.value("bankTime").toString();
|
||
obj["bankTransactionId"] = txMap.value("bankTrExternalId").toString();
|
||
|
||
worker->setDeviceId(accountMap.value("deviceId").toString());
|
||
worker->addExtra("lastNumbers", accountMap.value("lastNumbers").toString());
|
||
worker->addExtra("transactionId", txMap.value("id").toInt());
|
||
worker->setPayload(QJsonDocument(obj).toJson());
|
||
worker->moveToThread(thread);
|
||
QObject::connect(thread, &QThread::started, worker, &NetworkService::insertTransactionData);
|
||
QObject::connect(worker, &NetworkService::finished, thread, &QThread::quit);
|
||
QObject::connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
|
||
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||
thread->start();
|
||
}
|
||
|
||
void OzonHelpersBridge::postAccountsData(const QVariantList &accounts) {
|
||
if (accounts.isEmpty()) return;
|
||
auto *thread = new QThread;
|
||
auto *worker = new NetworkService;
|
||
|
||
QJsonArray list;
|
||
QString deviceId;
|
||
for (const QVariant &v : accounts) {
|
||
const QVariantMap acc = v.toMap();
|
||
QJsonObject obj;
|
||
obj["appName"] = acc.value("appCode").toString();
|
||
obj["lastNumbers"] = acc.value("lastNumbers").toString();
|
||
obj["amount"] = acc.value("amount").toDouble();
|
||
obj["description"] = acc.value("description").toString();
|
||
obj["currency"] = acc.value("currency").toString();
|
||
obj["status"] = acc.value("status").toString();
|
||
list.append(obj);
|
||
if (deviceId.isEmpty()) deviceId = acc.value("deviceId").toString();
|
||
}
|
||
|
||
worker->setDeviceId(deviceId);
|
||
worker->setPayload(QJsonDocument(list).toJson());
|
||
worker->moveToThread(thread);
|
||
QObject::connect(thread, &QThread::started, worker, &NetworkService::postAccountsData);
|
||
QObject::connect(worker, &NetworkService::finished, thread, &QThread::quit);
|
||
QObject::connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
|
||
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||
thread->start();
|
||
}
|
||
|
||
} // namespace Scripting
|