- 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.
418 lines
19 KiB
C++
418 lines
19 KiB
C++
#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
|