diff --git a/android/rshb/ScreenXmlParser.cpp b/android/rshb/ScreenXmlParser.cpp index d4bed02..955c572 100644 --- a/android/rshb/ScreenXmlParser.cpp +++ b/android/rshb/ScreenXmlParser.cpp @@ -7,8 +7,12 @@ #include #include #include +#include +#include +#include "adb/AdbUtils.h" #include "android/xml/Node.h" +#include "db/TransactionInfo.h" ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) { } @@ -16,6 +20,16 @@ ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) { ScreenXmlParser::~ScreenXmlParser() = default; static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])"); +static const QRegularExpression onlyDigitsRegex(R"([^0-9])"); + +static const QRegularExpression rePhone(R"raw((\+7\(?\d{3}\)?[- ]?\d{3}[- ]?\d{2}[- ]?\d{2}))raw"); +static const QRegularExpression reFromBank(R"raw(из (.+?) на)raw"); +static const QRegularExpression reToBank(R"raw(в (.+?) через)raw"); +static const QRegularExpression reSbpId(R"(СБП(?: С2С)? ([A-Z0-9]{30,}))"); +static const QRegularExpression reIdAlt(R"(ID перевода (\d+))"); +static const QRegularExpression reDate(R"((?:Дата операции|дата) (\d{2}\.\d{2}\.\d{4}(?: \d{2}:\d{2}:\d{2})?))"); +static const QRegularExpression reSender(R"(от ([^\(]+))"); +static const QRegularExpression reNameAlt(R"(Перевод ([А-Яа-яЁё\s\*]+) в)"); Node ScreenXmlParser::tryToFindBannerOrDialog(const int width, const int height) { // 1. Ищем push banner - он перекрывает весь экран @@ -179,7 +193,9 @@ QList ScreenXmlParser::getLast2DaysTransactions() { selected="false" bounds="[952,159][1039,248]"/> */ void ScreenXmlParser::parseAndSaveXml(const QString &xml) { - m_xml = xml; + QDomDocument doc; + doc.setContent(xml); + m_xml = doc.documentElement(); m_nodes.clear(); QXmlStreamReader reader(xml); @@ -221,6 +237,95 @@ void ScreenXmlParser::parseAndSaveXml(const QString &xml) { } } +QString getDayOfWeekAsString(const int day_of_week) { + switch (day_of_week) { + case 1: return "понедельник"; + case 2: return "вторник"; + case 3: return "среда"; + case 4: return "четверг"; + case 5: return "пятница"; + case 6: return "суббота"; + case 7: return "воскресенье"; + default: return ""; + } +} + +QString getMonthAsString(const int month) { + switch (month) { + case 1: return "января"; + case 2: return "февраля"; + case 3: return "марта"; + case 4: return "апреля"; + case 5: return "мая"; + case 6: return "июня"; + case 7: return "июля"; + case 8: return "августа"; + case 9: return "сентября"; + case 10: return "октября"; + case 11: return "ноября"; + case 12: return "декабря"; + default: return ""; + } +} + +QString findSubInfo(const QDomElement &elem) { + if (elem.hasChildNodes()) { + QDomNodeList subInfoList = elem.childNodes(); + for (QDomNode subInfoNode: subInfoList) { + if (!subInfoNode.isElement()) continue; + QDomElement subInfo = subInfoNode.toElement(); + if (subInfo.attribute("index") == "1") { + return subInfo.attribute("text"); + } + } + } + return ""; +} + +bool hasTimeComponent(const QString &datetimeStr) { + QDateTime dt = QDateTime::fromString(datetimeStr, "dd.MM.yyyy HH:mm:ss"); + if (!dt.isValid()) { + return false; // Строка не соответствует формату даты и времени + } + return dt.time() != QTime(0, 0, 0); // Проверка, что время не равно 00:00:00 +} + +void parseTransferInfo(const QString &text, TransactionInfo &info) { + if (const auto m = reSender.match(text); m.hasMatch()) { + info.name = m.captured(1).trimmed(); + } else if (const auto m1 = reNameAlt.match(text); m1.hasMatch()) { + info.name = m1.captured(1).trimmed(); + } + + if (const auto m = rePhone.match(text); m.hasMatch()) { + info.phone = m.captured(1).trimmed().remove(onlyDigitsRegex); + } + + if (const auto m = reFromBank.match(text); m.hasMatch()) { + if (const QString bank = m.captured(1).trimmed(); !bank.isEmpty()) { + info.bankName = bank; + } + } else if (const auto m1 = reToBank.match(text); m1.hasMatch()) { + if (const QString bank = m1.captured(1).trimmed(); !bank.isEmpty()) { + info.bankName = bank; + } + } + + if (const auto m = reSbpId.match(text); m.hasMatch()) { + info.trExternalId = m.captured(1); + } else if (const auto m1 = reIdAlt.match(text); m1.hasMatch()) { + info.trExternalId = m1.captured(1); + } + + if (const auto m = reDate.match(text); m.hasMatch()) { + const QString date = m.captured(1); + // если не пустая и дата у нас с 00:00 + if (hasTimeComponent(date)) { + info.bankTime = date; + } + } +} + void traverseNodes(const QDomNode &node) { if (node.isElement()) { QDomElement elem = node.toElement(); @@ -231,68 +336,85 @@ void traverseNodes(const QDomNode &node) { if (elem.tagName() == "node" && elem.attribute("class") == "android.widget.Button" && elem.attribute("text") == "Детали операции") { - // Ищем сиблингов от текущего узла и далее - QDomNodeList siblings = elem.parentNode().childNodes(); - - // Получение строки для анализа - // QString xmlString; - // QTextStream stream(&xmlString); - // elem.parentNode().save(stream, 0); - // qDebug() << xmlString; - // return; - - for (int j = 0; j < siblings.count(); ++j) { - QDomNode sib = siblings.at(j); - if (!sib.isElement()) continue; - QDomElement sibElem = sib.toElement(); + // Все что ниже деталей операции + QDomNodeList detailInfo = elem.parentNode().childNodes(); - qDebug() << "Node:" - << sibElem.attribute("index") - << sibElem.attribute("text"); - // << sibElem.attribute("class"); + for (int j = 0; j < detailInfo.count(); ++j) { + QDomNode info = detailInfo.at(j); + if (!info.isElement()) continue; + QDomElement infoElement = info.toElement(); + QString index = infoElement.attribute("index"); + switch (index.toInt()) { + case 2: + qDebug() << "Дата операции:" << findSubInfo(infoElement); + continue; + case 5: + qDebug() << "Название:" << findSubInfo(infoElement); + qDebug() << "-------------------------------------"; + // parseTransfer(findSubInfo(infoElement)); + continue; + case 7: + qDebug() << "Комиссия:" << findSubInfo(infoElement);; + continue; + default: + qDebug() << "Прочее:" << findSubInfo(infoElement);; + continue; + } - if (sibElem.hasChildNodes()) { - QDomNodeList sibChildren = sibElem.childNodes(); - for (int k = 0; k < sibChildren.count(); ++k) { - QDomNode sibChild = sibChildren.at(k); - if (!sibChild.isElement()) continue; - QDomElement sibChildElem = sibChild.toElement(); - qDebug() << "... Node:" - << sibChildElem.attribute("index") - << sibChildElem.attribute("text"); - // << sibChildElem.attribute("class"); + /** + Node: "2" "" + ... Node: "0" "Дата операции" + ... Node: "1" "30.04.2025 17:46:29" + Node: "5" "" + ... Node: "0" "Название" + ... Node: "1" "DBO TRANSFER RSHB INTERNET-BANK" + Node: "7" "" + ... Node: "0" "Комиссия" + ... Node: "1" "50 ₽" + */ - - if (sibChildElem.hasChildNodes()) { - QDomNodeList sibChildren2 = sibChildElem.childNodes(); - for (int k = 0; k < sibChildren2.count(); ++k) { - QDomNode sibChild2 = sibChildren2.at(k); - if (!sibChild2.isElement()) continue; - QDomElement sibChildElem2 = sibChild2.toElement(); - - qDebug() << "...... Node:" - << sibChildElem2.attribute("index") - << sibChildElem2.attribute("text"); - // << sibChildElem2.attribute("class"); - - if (sibChildElem2.hasChildNodes()) { - QDomNodeList sibChildren3 = sibChildElem2.childNodes(); - for (int k = 0; k < sibChildren3.count(); ++k) { - QDomNode sibChild3 = sibChildren3.at(k); - if (!sibChild3.isElement()) continue; - QDomElement sibChildElem3 = sibChild3.toElement(); - - qDebug() << "......... Node:" - << sibChildElem3.attribute("index") - << sibChildElem3.attribute("text"); - // << sibChildElem2.attribute("class"); - } - } - } - } - } - } + // if (sibElem.hasChildNodes()) { + // QDomNodeList sibChildren = sibElem.childNodes(); + // for (int k = 0; k < sibChildren.count(); ++k) { + // QDomNode sibChild = sibChildren.at(k); + // if (!sibChild.isElement()) continue; + // QDomElement sibChildElem = sibChild.toElement(); + // qDebug() << "... Node:" + // << sibChildElem.attribute("index") + // << sibChildElem.attribute("text"); + // // << sibChildElem.attribute("class"); + // + // + // if (sibChildElem.hasChildNodes()) { + // QDomNodeList sibChildren2 = sibChildElem.childNodes(); + // for (int k = 0; k < sibChildren2.count(); ++k) { + // QDomNode sibChild2 = sibChildren2.at(k); + // if (!sibChild2.isElement()) continue; + // QDomElement sibChildElem2 = sibChild2.toElement(); + // + // qDebug() << "...... Node:" + // << sibChildElem2.attribute("index") + // << sibChildElem2.attribute("text"); + // // << sibChildElem2.attribute("class"); + // + // if (sibChildElem2.hasChildNodes()) { + // QDomNodeList sibChildren3 = sibChildElem2.childNodes(); + // for (int k = 0; k < sibChildren3.count(); ++k) { + // QDomNode sibChild3 = sibChildren3.at(k); + // if (!sibChild3.isElement()) continue; + // QDomElement sibChildElem3 = sibChild3.toElement(); + // + // qDebug() << "......... Node:" + // << sibChildElem3.attribute("index") + // << sibChildElem3.attribute("text"); + // // << sibChildElem2.attribute("class"); + // } + // } + // } + // } + // } + // } } } } @@ -306,8 +428,286 @@ void traverseNodes(const QDomNode &node) { } } +QDomNode findNode(const QDomNode &node, const QString &text) { + if (node.isElement()) { + QString nodeText = node.toElement().attribute("text"); + if (nodeText.toLower().startsWith(text)) { + return node; + } + } + + + QDomNode child = node.firstChild(); + while (!child.isNull()) { + QDomNode result = findNode(child, text); + if (!result.isNull()) { + return result; + } + child = child.nextSibling(); + } + return {}; +} + +float extractAmounts(const QString &text) { + // Разрешаем пробелы внутри чисел, захватываем "рубль", "рубля", "рублей" и т.д. + // QRegularExpression re(R"((Плюс|Минус)\s+([\d\s]+)\s+рубл\w*\s+(\d+)\s+копе\w*)"); + QRegularExpression re( + R"((Плюс|Минус)\s+([\d\s]+)\s+рубл\w*\s+(\d+)\s+копе\w*)", + QRegularExpression::UseUnicodePropertiesOption + ); + + QRegularExpressionMatch match = re.match(text); + if (match.hasMatch()) { + QString signStr = match.captured(1); // "Плюс" или "Минус" + QString rublesStr = match.captured(2).remove(' '); // "1 000" → "1000" + int rubles = rublesStr.toInt(); + int kopecks = match.captured(3).toInt(); + + int totalKopecks = rubles * 100 + kopecks; + if (signStr == "Минус") { + totalKopecks *= -1; + } + + return totalKopecks / 100.0f; + } + return 0; +} + +QString beforeAmount(const QString &text) { + int index = text.indexOf(QRegularExpression("Плюс|Минус")); + if (index != -1) { + return text.left(index).trimmed(); + } + return text; // если не найдено — вернуть всю строку +} + +QList ScreenXmlParser::parseTodayAndYesterdayHistory() { + QTimeZone tz("Europe/Moscow"); + QDate today = QDateTime::currentDateTime(tz).date(); + QDate yesterday = today.addDays(-1); + QDate beforeYesterday = today.addDays(-2); + + + QString todayTitle = QString("сегодня, %1").arg(getDayOfWeekAsString(today.dayOfWeek())); + QString yesterdayTitle = QString("вчера, %1").arg(getDayOfWeekAsString(yesterday.dayOfWeek())); + QString beforeYesterdayTitle = QString("%1 %2, %3").arg( + QString::number(beforeYesterday.day()), + getMonthAsString(beforeYesterday.month()), + getDayOfWeekAsString(beforeYesterday.dayOfWeek()) + ); + + QDomNode todayNode = findNode(m_xml, todayTitle); + QDomNode yesterdayNode = findNode(m_xml, yesterdayTitle); + + QDomNode parentNode; + if (!yesterdayNode.isNull()) { + parentNode = yesterdayNode.parentNode(); + } else if (!todayNode.isNull()) { + parentNode = todayNode.parentNode(); + } + + + QList transactions; + if (!parentNode.isNull()) { + for (QDomNode transactionNode: parentNode.childNodes()) { + QString transactionShortText = transactionNode.toElement().attribute("text"); + + const QString titleLowerCase = transactionShortText.toLower(); + if (titleLowerCase.startsWith(beforeYesterdayTitle)) { + // только сегодня и вчера парсим, позавчера не трогаем + break; + } + + QDate date = {}; + if (titleLowerCase.startsWith(todayTitle)) { + date = today; + transactionShortText = transactionShortText.mid(todayTitle.length() + 1); + } + if (titleLowerCase.startsWith(yesterdayTitle)) { + date = yesterday; + transactionShortText = transactionShortText.mid(yesterdayTitle.length() + 1); + } + + if (transactionShortText.contains("ЕСПП")) { + continue; + } + + float amount = extractAmounts(transactionShortText); + QString shortDesc = beforeAmount(transactionShortText); + + TransactionInfo info; + info.amount = amount; + info.bankTime = date.toString("dd.MM.yyyy"); + info.shortDescription = shortDesc; + + transactions.append(info); + } + } + return transactions; +} + +void parse2daysHistory(const QDomNode &node) { + QTimeZone tz("Europe/Moscow"); + QDate today = QDateTime::currentDateTime(tz).date(); + QDate tomorrow = today.addDays(-1); + QDate after2Days = today.addDays(-2); + + QString todayTitle = QString("сегодня, %1").arg(getDayOfWeekAsString(today.dayOfWeek())); + QDomNode todayNode = findNode(node, todayTitle); + if (todayNode.isNull()) { + } else { + qDebug() << "todayNode: " << todayNode.toElement().attribute("text"); + } + + QString tomorrowTitle = QString("вчера, %1").arg(getDayOfWeekAsString(tomorrow.dayOfWeek())); + QDomNode tomorrowNode = findNode(node, tomorrowTitle); + if (tomorrowNode.isNull()) { + } else { + qDebug() << "tomorrowNode: " << tomorrowNode.toElement().attribute("text"); + } + + // 30 апреля, Среда + QString after2DaysTitle = QString("%1 %2, %3").arg( + QString::number(after2Days.day()), + getMonthAsString(after2Days.month()), + getDayOfWeekAsString(after2Days.dayOfWeek())); + QDomNode after2DaysNode = findNode(node, after2DaysTitle); + if (after2DaysNode.isNull()) { + qDebug() << "No after2DaysNode"; + } else { + qDebug() << "after2DaysNode: " << after2DaysNode.toElement().attribute("text"); + } + + QDomNode parentNode; + if (!todayNode.isNull() || !tomorrowNode.isNull() || !after2DaysNode.isNull()) { + if (!todayNode.isNull()) { + parentNode = todayNode.parentNode(); + } + if (!tomorrowNode.isNull()) { + parentNode = tomorrowNode.parentNode(); + } + if (!after2DaysNode.isNull()) { + parentNode = after2DaysNode.parentNode(); + } + } + + + if (!parentNode.isNull()) { + for (QDomNode child_node: parentNode.childNodes()) { + QString child_text = child_node.toElement().attribute("text"); + if (child_text.toLower().startsWith(todayTitle)) { + qDebug() << "=== Today: " << todayTitle; + qDebug() << "node: " << beforeAmount(child_text.mid(todayTitle.length() + 1)); + qDebug() << "--> sum: " << QString::number(extractAmounts(child_text), 'f', 2); + // qDebug() << ""; + continue; + } + if (child_text.toLower().startsWith(tomorrowTitle)) { + qDebug() << "=== Yesterday: " << todayTitle; + qDebug() << "node: " << beforeAmount(child_text.mid(tomorrowTitle.length() + 1)); + qDebug() << "--> sum: " << QString::number(extractAmounts(child_text), 'f', 2); + // qDebug() << ""; + continue; + } + if (child_text.toLower().startsWith(after2DaysTitle)) { + qDebug() << "=== Before yesterday: " << todayTitle; + qDebug() << "node: " << beforeAmount(child_text.mid(after2DaysTitle.length() + 1)); + qDebug() << "--> sum: " << QString::number(extractAmounts(child_text), 'f', 2); + // qDebug() << ""; + continue; + } + + qDebug() << "node: " << beforeAmount(child_node.toElement().attribute("text")); + qDebug() << "--> sum: " << + QString::number(extractAmounts(child_node.toElement().attribute("text")), 'f', 2); + // qDebug() << ""; + } + } + + + // QString d = today.day() > 9 ? QString::number(today.day()) : "0" + QString::number(today.day()); + // QString m = today.month() > 9 ? QString::number(today.month()) : "0" + QString::number(today.month()); + // QString y = QString::number(today.year()); + // QString w = getDayOfWeekAsString(today.dayOfWeek()); + // QString mm = getMonthAsString(today.month()); + // qDebug() << "Date:" << d << m << y << w << mm; + + + // QString d2 = after2Days.day() > 9 ? QString::number(after2Days.day()) : "0" + QString::number(after2Days.day()); + // QString m2 = after2Days.month() > 9 + // ? QString::number(after2Days.month()) + // : "0" + QString::number(after2Days.month()); + // QString y2 = QString::number(after2Days.year()); + // QString w2 = getDayOfWeekAsString(after2Days.dayOfWeek()); + // QString mm2 = getMonthAsString(after2Days.month()); + // qDebug() << "Date:" << d2 << m2 << y2 << w2 << mm2; +} + +TransactionInfo parseExpandedInfoHistory(const QDomElement &root) { + if (root.isElement()) { + QDomElement elem = root.toElement(); + if (elem.tagName() == "node" + && elem.attribute("class") == "android.widget.Button" + && elem.attribute("text") == "Детали операции" + ) { + TransactionInfo transaction; + // Все что ниже деталей операции + QDomNodeList detailInfo = elem.parentNode().childNodes(); + + for (int i = 0; i < detailInfo.count(); ++i) { + QDomNode info = detailInfo.at(i); + if (!info.isElement()) continue; + + switch (QDomElement e = info.toElement(); e.attribute("index").toInt()) { + case 2: + // Дата операции + transaction.bankTime = findSubInfo(e); + break; + case 5: + transaction.description = findSubInfo(e); + parseTransferInfo(transaction.description, transaction); + break; + case 7: + // Комиссия + transaction.fee = findSubInfo(e).remove(onlyDigitsRegex).toFloat(); + break; + default: + // qDebug() << "Прочее:" << findSubInfo(e); + break; + } + } + return transaction; + } + } + + // Рекурсивно обходим всех детей + QDomNode child = root.firstChild(); + while (!child.isNull()) { + if (TransactionInfo info = parseExpandedInfoHistory(child.toElement()); !info.bankTime.isEmpty()) { + return info; + } + child = child.nextSibling(); + } + + return {}; +} + +TransactionInfo ScreenXmlParser::parseTransactionInfoHistory(int screenWidth, int screenHeight) { + // 1. Распарсить описание транзакции + TransactionInfo transaction = parseExpandedInfoHistory(m_xml); + // 1. Найти статус: Исполнен | Операция исполнена + transaction.status = TransactionStatus::Wait; + Node status1 = findTextNode("Исполнен", 0, 0, screenWidth, screenHeight / 2); + Node status2 = findTextNode("Операция исполнена", 0, 0, screenWidth, screenHeight / 2); + if (!status1.isEmpty() || !status2.isEmpty()) { + transaction.status = TransactionStatus::Complete; + } + return transaction; +} + void ScreenXmlParser::test() { - QFile file("assets/rshb/pay/pay_by_phone_history_expanded.xml"); + return; + QFile file("assets/rshb/test/spb_test_send.xml"); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Cannot open file"; } @@ -321,5 +721,5 @@ void ScreenXmlParser::test() { qDebug() << "Start parsing XML:"; QDomElement root = doc.documentElement(); - traverseNodes(root); + // ЕСПП } diff --git a/android/rshb/ScreenXmlParser.h b/android/rshb/ScreenXmlParser.h index 4cda0f0..624b389 100644 --- a/android/rshb/ScreenXmlParser.h +++ b/android/rshb/ScreenXmlParser.h @@ -1,7 +1,9 @@ #pragma once #include +#include #include "android/xml/Node.h" +#include "db/TransactionInfo.h" class ScreenXmlParser final : public QObject { Q_OBJECT @@ -31,11 +33,14 @@ public: void parseAndSaveXml(const QString &xml); + TransactionInfo parseTransactionInfoHistory(int screenWidth, int screenHeight); + QList parseTodayAndYesterdayHistory(); + void test(); private: QList m_nodes; - QString m_xml; + QDomElement m_xml; QList findPinCodeNodes(const QString &pinCode); };