#include "ScriptBridges.h" #include #include #include #include #include #include #include #include #include #include #include #include #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(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()) 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(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 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 &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 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 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 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 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