Implemented `GetCardInfoScript`, `GetProfileInfoScript`, and `GetLastTransactionsScript` to automate data extraction workflows for Ozon platform. Includes support for XML parsing, ad banner handling, card navigation, profile parsing, and transaction retrieval. Added related asset files.
421 lines
16 KiB
C++
421 lines
16 KiB
C++
#include "DeviceScreener.h"
|
||
|
||
#include <QSettings>
|
||
#include <QProcess>
|
||
#include <QFile>
|
||
#include <QBuffer>
|
||
#include <qeventloop.h>
|
||
#include <QThread>
|
||
#include <QImage>
|
||
#include <QHttpMultiPart>
|
||
#include <QJsonObject>
|
||
#include <QJsonArray>
|
||
#include <QJsonDocument>
|
||
#include <QNetworkReply>
|
||
#include <QNetworkRequest>
|
||
#include <dao/DeviceDAO.h>
|
||
#include <db/DeviceInfo.h>
|
||
|
||
#include "adb/AdbUtils.h"
|
||
#include "dao/ApplicationDAO.h"
|
||
|
||
DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) {
|
||
const QSettings settings("config.ini", QSettings::IniFormat);
|
||
m_urlAppUpdate = settings.value("net/appInfoUpdate").toString();
|
||
m_token = settings.value("common/token").toString();
|
||
m_apiBase = settings.value("net/apiBase").toString();
|
||
}
|
||
|
||
DeviceScreener::~DeviceScreener() = default;
|
||
|
||
void DeviceScreener::postDevicesData(const QList<DeviceInfo> &devices) {
|
||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||
|
||
QHttpPart jsonPart;
|
||
jsonPart.setHeader(QNetworkRequest::ContentTypeHeader, "application/json; charset=UTF-8");
|
||
jsonPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="metadata")");
|
||
|
||
QJsonArray list;
|
||
for (const DeviceInfo &device: devices) {
|
||
QJsonObject obj;
|
||
obj["uniqueName"] = device.id;
|
||
obj["status"] = device.status;
|
||
obj["name"] = device.name;
|
||
obj["screenWidth"] = device.screenWidth;
|
||
obj["screenHeight"] = device.screenHeight;
|
||
obj["battery"] = device.battery;
|
||
|
||
list.append(obj);
|
||
}
|
||
|
||
jsonPart.setBody(QJsonDocument(list).toJson());
|
||
multiPart->append(jsonPart);
|
||
|
||
|
||
for (const DeviceInfo &device: devices) {
|
||
QHttpPart imagePart;
|
||
QString imageHeader = QStringLiteral("form-data; name=files; filename=%1.jpg").arg(device.id);
|
||
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
|
||
imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, imageHeader.toUtf8());
|
||
imagePart.setBody(device.image);
|
||
multiPart->append(imagePart);
|
||
}
|
||
|
||
QHttpPart uniquePart;
|
||
uniquePart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="token")");
|
||
uniquePart.setBody(m_token.toUtf8());
|
||
|
||
multiPart->append(uniquePart);
|
||
|
||
QNetworkAccessManager manager; // создаём внутри потока
|
||
QUrl url(m_urlAppUpdate);
|
||
QNetworkRequest request(url);
|
||
|
||
// делаем запрос
|
||
QNetworkReply *reply = manager.post(request, multiPart);
|
||
multiPart->setParent(reply); // multiPart удалится, когда придёт ответ
|
||
|
||
// локальный цикл — ждём, пока придёт finished()
|
||
QEventLoop loop;
|
||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||
QObject::connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
|
||
loop.quit();
|
||
});
|
||
loop.exec(); // <- здесь и запускается event-loop потока
|
||
|
||
// обрабатываем результат
|
||
if (reply->error() == QNetworkReply::NoError) {
|
||
QByteArray raw = reply->readAll();
|
||
QJsonParseError err;
|
||
QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
|
||
if (err.error == QJsonParseError::NoError && doc.isObject()) {
|
||
QJsonObject root = doc.object();
|
||
if (!root.value("success").toBool()) {
|
||
qWarning() << root;
|
||
}
|
||
}
|
||
} else {
|
||
qWarning() << "Response error:" << reply->errorString() << m_urlAppUpdate;
|
||
}
|
||
reply->deleteLater();
|
||
}
|
||
|
||
QJsonValue DeviceScreener::apiPost(const QString &path, const QJsonObject &body) {
|
||
if (!m_manager) m_manager = new QNetworkAccessManager(this);
|
||
|
||
const QSettings settings("config.ini", QSettings::IniFormat);
|
||
QNetworkRequest request(QUrl(m_apiBase + path));
|
||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||
request.setRawHeader("Authorization",
|
||
("Bearer " + settings.value("common/access_token").toString()).toUtf8());
|
||
|
||
QNetworkReply *reply = m_manager->post(request, QJsonDocument(body).toJson());
|
||
QEventLoop loop;
|
||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
|
||
loop.exec();
|
||
|
||
QJsonValue result;
|
||
if (reply->error() == QNetworkReply::NoError) {
|
||
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
||
if (doc.isObject()) {
|
||
const QJsonObject root = doc.object();
|
||
if (root["success"].toBool()) result = root["data"];
|
||
else qCritical() << "DeviceScreener apiPost error:" << root;
|
||
}
|
||
} else {
|
||
qCritical() << "DeviceScreener apiPost network error:" << reply->errorString();
|
||
}
|
||
reply->deleteLater();
|
||
return result;
|
||
}
|
||
|
||
void DeviceScreener::apiPatch(const QString &path, const QJsonObject &body) {
|
||
if (!m_manager) m_manager = new QNetworkAccessManager(this);
|
||
|
||
const QSettings settings("config.ini", QSettings::IniFormat);
|
||
QNetworkRequest request(QUrl(m_apiBase + path));
|
||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||
request.setRawHeader("Authorization",
|
||
("Bearer " + settings.value("common/access_token").toString()).toUtf8());
|
||
|
||
QNetworkReply *reply = m_manager->sendCustomRequest(request, "PATCH", QJsonDocument(body).toJson());
|
||
QEventLoop loop;
|
||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
|
||
loop.exec();
|
||
|
||
if (reply->error() != QNetworkReply::NoError) {
|
||
qCritical() << "DeviceScreener apiPatch network error:" << reply->errorString();
|
||
}
|
||
reply->deleteLater();
|
||
}
|
||
|
||
QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) {
|
||
const QSettings settings("config.ini", QSettings::IniFormat);
|
||
const QString desktopId = settings.value("common/desktop_id").toString();
|
||
if (desktopId.isEmpty()) {
|
||
qWarning() << "createDeviceOnApi: desktop_id не задан";
|
||
return {};
|
||
}
|
||
|
||
QJsonObject body;
|
||
body["desktop_id"] = desktopId;
|
||
body["status"] = "ONLINE";
|
||
body["name"] = device.name;
|
||
body["screen_width"] = device.screenWidth;
|
||
body["screen_height"] = device.screenHeight;
|
||
body["battery"] = device.battery;
|
||
|
||
const QJsonValue result = apiPost("/api/v1/device/", body);
|
||
if (!result.isNull() && !result.isUndefined()) {
|
||
return result.toObject()["id"].toString();
|
||
}
|
||
return {};
|
||
}
|
||
|
||
void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray &image) {
|
||
if (!m_manager) m_manager = new QNetworkAccessManager(this);
|
||
|
||
const QSettings settings("config.ini", QSettings::IniFormat);
|
||
const QString path = "/api/v1/device/add_screenshot/" + apiId;
|
||
QNetworkRequest request(QUrl(m_apiBase + path));
|
||
request.setRawHeader("Authorization",
|
||
("Bearer " + settings.value("common/access_token").toString()).toUtf8());
|
||
|
||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||
QHttpPart filePart;
|
||
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
|
||
filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
|
||
QStringLiteral("form-data; name=\"file\"; filename=\"screenshot.jpg\""));
|
||
filePart.setBody(image);
|
||
multiPart->append(filePart);
|
||
|
||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||
multiPart->setParent(reply);
|
||
QEventLoop loop;
|
||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
|
||
loop.exec();
|
||
|
||
if (reply->error() != QNetworkReply::NoError) {
|
||
qWarning() << "postScreenshotToApi network error:" << reply->errorString();
|
||
} else {
|
||
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
||
if (doc.isObject() && doc.object()["success"].toBool()) {
|
||
qDebug() << "postScreenshotToApi done for device" << apiId;
|
||
} else {
|
||
qWarning() << "postScreenshotToApi failed for device" << apiId << ":" << doc.toJson(QJsonDocument::Compact);
|
||
}
|
||
}
|
||
reply->deleteLater();
|
||
}
|
||
|
||
void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) {
|
||
QJsonObject body;
|
||
body["status"] = (device.status == "device") ? "ONLINE" : "OFFLINE";
|
||
body["battery"] = device.battery;
|
||
body["update_time"] = QDateTime::currentSecsSinceEpoch();
|
||
apiPatch("/api/v1/device/" + device.apiId, body);
|
||
}
|
||
|
||
void DeviceScreener::start() {
|
||
QMap<QString, bool> bankChecked;
|
||
while (m_running) {
|
||
QList<QPair<QString, QString> > devices = readAdbDevices();
|
||
QStringList onlineIds;
|
||
QList<DeviceInfo> deviceInfos;
|
||
for (const auto &[id, status]: devices) {
|
||
DeviceInfo device;
|
||
device.id = id;
|
||
|
||
if (status == "device") {
|
||
onlineIds << device.id;
|
||
const bool isChecked = bankChecked.contains(id);
|
||
device = readDeviceInfo(id, isChecked);
|
||
device.image = takeScreenshot(id);
|
||
if (!device.image.isEmpty()) {
|
||
if (!DeviceDAO::updateImage(id, device.image)) {
|
||
qDebug() << "Cant update screenshot";
|
||
}
|
||
}
|
||
if (!isChecked) {
|
||
ApplicationDAO::activateAppsForDevice(id);
|
||
}
|
||
bankChecked[id] = true;
|
||
}
|
||
device.status = status;
|
||
device.updateTime = QDateTime::currentDateTime();
|
||
deviceInfos.append(device);
|
||
if (status == "device") {
|
||
if (!DeviceDAO::upsertDevice(device)) {
|
||
qDebug() << "Cant update device";
|
||
}
|
||
}
|
||
|
||
// Синхронизация с API
|
||
DeviceInfo saved = DeviceDAO::getDeviceById(device.id);
|
||
if (saved.apiId.isEmpty()) {
|
||
// Устройство ещё не зарегистрировано — создаём
|
||
const QString apiId = createDeviceOnApi(device);
|
||
if (!apiId.isEmpty()) {
|
||
DeviceDAO::updateApiId(device.id, apiId);
|
||
qDebug() << "Device registered on API:" << device.id << "->" << apiId;
|
||
}
|
||
} else if (status == "device") {
|
||
// Обновляем не чаще раза в 60 секунд
|
||
const qint64 now = QDateTime::currentSecsSinceEpoch();
|
||
if (now - m_lastApiUpdate.value(device.id, 0) >= 60) {
|
||
device.apiId = saved.apiId;
|
||
updateDeviceOnApi(device);
|
||
if (!device.image.isEmpty()) {
|
||
postScreenshotToApi(saved.apiId, device.image);
|
||
}
|
||
m_lastApiUpdate[device.id] = now;
|
||
}
|
||
}
|
||
}
|
||
|
||
// if (!deviceInfos.empty()) {
|
||
// postDevicesData(deviceInfos);
|
||
// }
|
||
|
||
if (!DeviceDAO::markDevicesOfflineExcept(onlineIds)) {
|
||
qDebug() << "Cant mark devices offline";
|
||
}
|
||
|
||
// Деактивировать приложения для устройств, которые ушли в офлайн
|
||
QStringList offlineIds;
|
||
for (auto it = bankChecked.begin(); it != bankChecked.end();) {
|
||
if (!onlineIds.contains(it.key())) {
|
||
ApplicationDAO::deactivateAppsForDevice(it.key());
|
||
it = bankChecked.erase(it);
|
||
} else {
|
||
++it;
|
||
}
|
||
}
|
||
|
||
QThread::msleep(1000);
|
||
}
|
||
emit finished();
|
||
}
|
||
|
||
QByteArray convertPngToJpeg(const QByteArray &pngData, const int quality = 90) {
|
||
QImage image;
|
||
if (!image.loadFromData(pngData, "PNG")) {
|
||
qWarning() << "Не удалось загрузить PNG из байт";
|
||
return {};
|
||
}
|
||
|
||
QByteArray jpgData;
|
||
QBuffer buffer(&jpgData);
|
||
buffer.open(QIODevice::WriteOnly);
|
||
image.save(&buffer, "JPG", quality);
|
||
return jpgData;
|
||
}
|
||
|
||
QByteArray DeviceScreener::takeScreenshot(const QString &deviceId) {
|
||
QProcess process;
|
||
QStringList arguments;
|
||
|
||
arguments << "-s" << deviceId << "exec-out" << "screencap" << "-p";
|
||
process.start("adb", arguments);
|
||
|
||
if (!process.waitForFinished(5000)) {
|
||
qWarning() << "ADB скриншот не завершился для" << deviceId;
|
||
return {};
|
||
}
|
||
|
||
const QByteArray imageData = process.readAllStandardOutput();
|
||
|
||
if (imageData.isEmpty()) {
|
||
qWarning() << "Скриншот пустой для " << deviceId;
|
||
return {};
|
||
}
|
||
|
||
return convertPngToJpeg(imageData, 90);
|
||
}
|
||
|
||
QList<QPair<QString, QString> > DeviceScreener::readAdbDevices() {
|
||
QProcess process;
|
||
process.start("adb", QStringList() << "devices");
|
||
process.waitForFinished();
|
||
|
||
const QString output = process.readAllStandardOutput();
|
||
QList<QPair<QString, QString> > devices;
|
||
const QStringList lines = output.split('\n');
|
||
for (int i = 1; i < lines.size(); ++i) {
|
||
QString line = lines.at(i).trimmed();
|
||
|
||
if (line.isEmpty()) {
|
||
continue;
|
||
}
|
||
/**
|
||
* device — устройство готово к использованию.
|
||
* offline — устройство подключено, но не может быть использовано.
|
||
* unauthorized — устройство не авторизовано для работы с ADB (нужно подтвердить запрос на подключение на самом устройстве).
|
||
* recovery — устройство в режиме восстановления.
|
||
*/
|
||
// Разделяем строку на id и type по табуляции
|
||
if (QStringList parts = line.split('\t'); parts.size() == 2) {
|
||
const QString &id = parts.at(0);
|
||
const QString &status = parts.at(1);
|
||
devices.append(qMakePair(id, status));
|
||
}
|
||
}
|
||
return devices;
|
||
}
|
||
|
||
static const QRegularExpression screenRegex(R"(Physical size: (\d+)x(\d+))");
|
||
static const QRegularExpression batteryRegex(R"(level:\s*(\d+))");
|
||
|
||
DeviceInfo DeviceScreener::readDeviceInfo(const QString &deviceId, const bool isChecked) {
|
||
DeviceInfo info;
|
||
info.id = deviceId;
|
||
info.screenWidth = 0;
|
||
info.screenHeight = 0;
|
||
info.battery = 0;
|
||
|
||
auto runAdbCommand = [&](const QStringList &args) -> QString {
|
||
QProcess process;
|
||
process.start("adb", QStringList{"-s", deviceId} + args);
|
||
process.waitForFinished();
|
||
return QString::fromUtf8(process.readAllStandardOutput()).trimmed();
|
||
};
|
||
|
||
info.name = runAdbCommand({"shell", "getprop", "ro.product.model"});
|
||
info.android = runAdbCommand({"shell", "getprop", "ro.build.version.release"});
|
||
|
||
// Размер экрана
|
||
const QString screenOutput = runAdbCommand({"shell", "wm", "size"});
|
||
QRegularExpressionMatch match = screenRegex.match(screenOutput);
|
||
if (match.hasMatch()) {
|
||
info.screenWidth = match.captured(1).toInt();
|
||
info.screenHeight = match.captured(2).toInt();
|
||
}
|
||
|
||
// Уровень батареи
|
||
const QString batteryOutput = runAdbCommand({"shell", "dumpsys", "battery"});
|
||
match = batteryRegex.match(batteryOutput);
|
||
if (match.hasMatch()) {
|
||
info.battery = match.captured(1).toInt();
|
||
}
|
||
|
||
if (!isChecked) {
|
||
QSet<QString> apps = AdbUtils::getListApps(deviceId);
|
||
if (!ApplicationDAO::checkInstalledApps(deviceId, apps)) {
|
||
qWarning() << "Cant check installed apps";
|
||
} else {
|
||
qDebug() << "Apps checked: " + deviceId;
|
||
}
|
||
}
|
||
|
||
info.updateTime = QDateTime::currentDateTime();
|
||
return info;
|
||
}
|
||
|
||
|
||
void DeviceScreener::stop() {
|
||
m_running = false;
|
||
}
|