1
0
forked from BRT/arc
arc/services/scripting/ScriptBridges.cpp
slava cae345cf61 Implement JS-based remote scripting framework for dynamic script updates:
- Add Proof of Concept (PoC) for executing JavaScript scripts fetched remotely.
- Integrate `Black::GetProfileInfoScript` logic into a JS version with fallback to C++ on errors.
- Extend `ScreenXmlParser` with additional JS-to-C++ bridge methods.
- Update `config.ini` for scripting configuration.
- Ensure scripts are securely fetched with SHA-256 validation and use an atomic caching mechanism.
- Add developer tools for debugging JS scripts and enable easier script updates without app re-deployment.
2026-05-13 15:42:10 +07:00

418 lines
19 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

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

#include "ScriptBridges.h"
#include <QBuffer>
#include <QByteArray>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSettings>
#include <QString>
#include <QThread>
#include <QRegularExpression>
#include "adb/AdbUtils.h"
#include "black/ScreenXmlParser.h"
#include "android/xml/Node.h"
#include "db/BankProfileInfo.h"
#include "db/DeviceInfo.h"
#include "db/MaterialInfo.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/MaterialDAO.h"
#include "dump/TaskDumpRecorder.h"
#include "net/NetworkService.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["amount"] = a.amount;
m["currency"] = a.currency;
m["description"] = a.description;
m["status"] = materialStatusToString(a.status);
return m;
}
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;
}
// ─── 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;
}
// ─── 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();
}
}
} // namespace Scripting