#include "ScreenXmlBirbankParser.h" #include #include #include #include #include #include #include #include #include "adb/AdbUtils.h" #include "android/xml/Node.h" #include "db/AccountInfo.h" #include "db/TransactionInfo.h" #include "time/DateUtils.h" ScreenXmlBirbankParser::ScreenXmlBirbankParser(QObject *parent) : QObject(parent) { } ScreenXmlBirbankParser::~ScreenXmlBirbankParser() = default; static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])"); static const QRegularExpression onlyDigitsRegex(R"([^0-9])"); static const QRegularExpression onlyDigitsAndDotRegex(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\*]+) в)"); static const QRegularExpression reCardData(R"(цифрами\s+(\d\s\d\s\d\s\d).*?Сумма:\s([\d\s]+)\sруб)"); Node ScreenXmlBirbankParser::tryToFindBannerOrDialog(const int width, const int height) { // 1. Ищем push banner - он перекрывает весь экран Node first = m_nodes[0]; for (const Node &node: m_nodes) { // Если это однозначно, что пуш-баннер if (node.resourceId == "aft-popup-pushNotification-iconClose" && node.className == "android.widget.Button") { return node; } // Если есть кнопка "Позже" if (node.text.trimmed() == "Позже" && node.className == "android.widget.Button") { return node; } // Если нашли пустую кнпоку if (node.className == "android.widget.Button" && node.clickable && node.text.isEmpty() && node.contentDesc.isEmpty()) { // if (node.x() > width / 2 && node.y() < width / 2) { return node; } } } return {}; } Node ScreenXmlBirbankParser::findAccountButton(const QString &numbers) { // TODO возможно это не так, надо уточнять return findTextNode(QString("LEVENT - %1").arg(numbers)); } /** * Смотрим что есть текс "Мы заботимся о вашей безопасности" и * кнопка "Отложить" * @return координаты кнопки для закрытия */ Node ScreenXmlBirbankParser::tryToFindSecurityBanner() { Node text; Node closeBtn; for (const Node &node: m_nodes) { if (node.text.trimmed() == "Мы заботимся о вашей безопасности" && node.className == "android.app.Dialog") { text = node; } if (node.text.trimmed() == "Отложить" && node.className == "android.widget.Button") { closeBtn = node; } } // Не нужно проверять все, если хотя бы одна координата есть if (!text.isEmpty() && !closeBtn.isEmpty()) { return closeBtn; } return {}; } Node ScreenXmlBirbankParser::tryToFindSelectBottomSheet() { Node closeBtn; for (const Node &node: m_nodes) { if ((node.text.trimmed() == "Отмена" || node.text.trimmed() == "Отложить") && node.className == "android.widget.Button") { closeBtn = node; } } // Не нужно проверять все, если хотя бы одна координата есть if (!closeBtn.isEmpty()) { return closeBtn; } return {}; } Node ScreenXmlBirbankParser::findTextNode(const QString &text) { for (const Node &node: m_nodes) { if (node.className == "android.widget.TextView" && node.text.trimmed() == text) { return node; } } return {}; } Node ScreenXmlBirbankParser::findTextNode(const QString &text, const int x1, const int y1, const int x2, const int y2) { for (const Node &node: m_nodes) { if (node.className == "android.widget.TextView" && node.text.trimmed() == text && node.x() >= x1 && node.x() <= x2 && node.y() >= y1 && node.y() <= y2) { return node; } } return {}; } Node ScreenXmlBirbankParser::findEditText(const QString &text) { for (const Node &node: m_nodes) { if (node.className == "android.widget.EditText" && node.text == text) { return node; } } return {}; } Node ScreenXmlBirbankParser::findEditTextByResourceId(const QString &resourceId) { for (const Node &node: m_nodes) { if (node.className == "android.widget.EditText" && node.resourceId == resourceId) { return node; } } return {}; } Node ScreenXmlBirbankParser::findButtonNode(const QString &text, const bool contains) { for (const Node &node: m_nodes) { if (contains) { if (node.className == "android.widget.Button" && node.text.contains(text)) { return node; } } else { if (node.className == "android.widget.Button" && node.text.trimmed() == text) { return node; } } } return {}; } bool ScreenXmlBirbankParser::isHomeScreen() { Node activitiesBtn; for (const Node &node: m_nodes) { if (node.text.trimmed() == "Карты и счета") { activitiesBtn = node; } } return !activitiesBtn.isEmpty(); } QList ScreenXmlBirbankParser::findPinCodeNodes(const QString &pinCode) { QMap buttons; const QSet numberSet = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; for (const Node &node: m_nodes) { const QString &text = node.text.trimmed(); if (node.className == "android.widget.TextView" && numberSet.contains(text)) { buttons[text] = node; } } QList nodes; for (int i = 0; i < pinCode.length(); ++i) { nodes.append(buttons[QString(pinCode.at(i))]); } return nodes; } QList ScreenXmlBirbankParser::getLast2DaysTransactions() { QList nodes; for (const Node &node: m_nodes) { if (node.className == "android.widget.Button" && node.text.contains("рубл") && (node.text.contains("Минус") || node.text.contains("Плюс"))) { nodes.append(node); } } return nodes; } /** * */ void ScreenXmlBirbankParser::parseAndSaveXml(const QString &xml) { QDomDocument doc; doc.setContent(xml); m_xml = doc.documentElement(); m_nodes.clear(); QXmlStreamReader reader(xml); while (!reader.atEnd()) { reader.readNext(); if (reader.isStartElement() && reader.name() == "node") { Node node = Node(); if (reader.attributes().hasAttribute("bounds")) { QString bounds = reader.attributes().value("bounds").toString(); if (QRegularExpressionMatch match = boundsRegex.match(bounds); match.hasMatch()) { node.setBounds( match.captured(1).toInt(), match.captured(2).toInt(), match.captured(3).toInt(), match.captured(4).toInt() ); } } if (reader.attributes().hasAttribute("text")) { node.text = reader.attributes().value("text").toString(); } if (reader.attributes().hasAttribute("content-desc")) { node.contentDesc = reader.attributes().value("content-desc").toString(); } if (reader.attributes().hasAttribute("class")) { node.className = reader.attributes().value("class").toString(); } if (reader.attributes().hasAttribute("clickable")) { node.clickable = reader.attributes().value("clickable").toString() == "true"; } if (reader.attributes().hasAttribute("focusable")) { node.focusable = reader.attributes().value("focusable").toString() == "true"; } if (reader.attributes().hasAttribute("resource-id")) { node.resourceId = reader.attributes().value("resource-id").toString(); } m_nodes.append(node); } } } QDomElement findSubElementBirbank(const QDomElement &elem, const QString &index) { if (elem.hasChildNodes()) { QDomNodeList subInfoList = elem.childNodes(); for (QDomNode subInfoNode: subInfoList) { if (!subInfoNode.isElement()) continue; QDomElement subInfo = subInfoNode.toElement(); if (subInfo.attribute("index") == index) { return subInfo; } } } return {}; } QString findSubInfoBirbank(const QDomElement &elem, const QString &index) { QDomElement subInfo = findSubElementBirbank(elem, index); if (!subInfo.isNull()) { return subInfo.attribute("text"); } return ""; } bool hasTimeComponentBirbank(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 } QString transformBankDateTimeBirbank(const QString &date) { QDateTime dt; if (date.contains(",")) { // 10.05.2025, 15:03 dt = QDateTime::fromString(date, "dd.MM.yyyy, HH:mm"); } else { dt = QDateTime::fromString(date, "dd.MM.yyyy HH:mm:ss"); } if (!dt.isValid()) { dt = QDateTime::fromString(date, "dd.MM.yyyy HH:mm"); } if (!dt.isValid()) { return date; // не распарсили } return dt.toString("dd.MM.yyyy HH:mm:ss"); } void parseTransferInfoBirbank(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.bankTrExternalId = m.captured(1); } else if (const auto m1 = reIdAlt.match(text); m1.hasMatch()) { info.bankTrExternalId = m1.captured(1); } if (const auto m = reDate.match(text); m.hasMatch()) { const QString date = m.captured(1); // если не пустая и дата у нас с 00:00 // if (hasTimeComponent2(date)) { info.bankTime = transformBankDateTimeBirbank(date); // } } } void traverseNodesBirbank(const QDomNode &node) { if (node.isElement()) { QDomElement elem = node.toElement(); if (node.isElement()) { QDomElement elem = node.toElement(); if (elem.tagName() == "node" && elem.attribute("class") == "android.widget.Button" && elem.attribute("text") == "Детали операции") { // Все что ниже деталей операции QDomNodeList detailInfo = elem.parentNode().childNodes(); 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() << "Дата операции:" << findSubInfoBirbank(infoElement, "1"); break; case 5: qDebug() << "Название:" << findSubInfoBirbank(infoElement, "1"); qDebug() << "-------------------------------------"; // parseTransfer(findSubInfo2(infoElement)); break; case 7: qDebug() << "Комиссия:" << findSubInfoBirbank(infoElement, "1"); break; default: qDebug() << "Прочее:" << findSubInfoBirbank(infoElement, "1"); break; } } } } } // Рекурсивно обходим всех детей QDomNode child = node.firstChild(); while (!child.isNull()) { traverseNodesBirbank(child); child = child.nextSibling(); } } QDomNode findNodeBirbank(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 = findNodeBirbank(child, text); if (!result.isNull()) { return result; } child = child.nextSibling(); } return {}; } float extractAmountsBirbank(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 beforeAmountBirbank(const QString &text) { int index = text.indexOf(QRegularExpression("Плюс|Минус")); if (index != -1) { return text.left(index).trimmed(); } return text; // если не найдено — вернуть всю строку } double parseAmountBirbank(const QString &amountAsString) { QString numberString = amountAsString; numberString.remove(QRegularExpression(R"([^\dточка])")); numberString.replace("точка", "."); double amount = numberString.toDouble(); if (amountAsString.startsWith("-")) { amount *= -1; } return amount; } QDomElement findElementWithPaymentsBirbank(const QDomElement &parent, const QString &resourceId) { QDomElement element = parent.firstChildElement(); while (!element.isNull()) { QDomElement child = element.firstChildElement(); while (!child.isNull()) { if (child.hasAttribute("resource-id") && child.attribute("resource-id") == resourceId) { return child; } child = child.nextSiblingElement(); } QDomElement found = findElementWithPaymentsBirbank(element, resourceId); if (!found.isNull()) { return found; } element = element.nextSiblingElement(); } return {}; } QDateTime getUtcFromBakuDate(const QString &date, const QString &time) { QDate baseDate; QTime parsedTime = QTime::fromString(time, "H:mm"); if (!parsedTime.isValid()) { qWarning() << "Невалидное время:" << time; return {}; } QString dateLower = date.trimmed().toLower(); if (dateLower == "сегодня") { baseDate = QDate::currentDate(); } else if (dateLower == "вчера") { baseDate = QDate::currentDate().addDays(-1); } else if (dateLower == "позавчера") { baseDate = QDate::currentDate().addDays(-2); } else { // Пробуем разобрать обычную дату, например "24 июля" QLocale ruLocale(QLocale::Russian, QLocale::Russia); int year = QDate::currentDate().year(); QString combined = QString("%1 %2 %3").arg(date).arg(year).arg(time); // "24 июля 2025 0:31" QDateTime dt = ruLocale.toDateTime(combined, "d MMMM yyyy H:mm"); if (!dt.isValid()) { qWarning() << "Ошибка разбора даты:" << combined; return {}; } dt.setTimeZone(QTimeZone("Asia/Baku")); return dt.toTimeZone(QTimeZone::utc()); } // Собираем и переводим в UTC, если дата относительная QDateTime bakuDateTime(baseDate, parsedTime, QTimeZone("Asia/Baku")); return bakuDateTime.toTimeZone(QTimeZone::utc()); } QList ScreenXmlBirbankParser::parsePositiveTransactions() { QList transactions; QDomElement child = findElementWithPaymentsBirbank(m_xml, "az.kapitalbank.mbanking:id/rv_pfm_operations"); // QDomElement child = findElementWithPaymentsBirbank(m_xml, "az.kapitalbank.mbanking:id/rv_operations"); QDomNodeList listView = child.childNodes(); qDebug() << listView.count(); if (!listView.isEmpty()) { QString date = ""; for (QDomNode node: listView) { const QDomNodeList trBoxGroup = node.childNodes(); if (trBoxGroup.count() > 0) { TransactionInfo transaction; for (QDomNode item: trBoxGroup) { QDomElement groupItem = item.toElement(); if ("az.kapitalbank.mbanking:id/tv_date" == groupItem.attribute("resource-id")) { date = groupItem.attribute("text"); } else if ("az.kapitalbank.mbanking:id/transaction_title" == groupItem.attribute("resource-id")) { transaction.description = groupItem.attribute("text"); } else if ("az.kapitalbank.mbanking:id/transaction_time" == groupItem.attribute("resource-id")) { transaction.bankTime = date + " " + groupItem.attribute("text"); transaction.completeTime = getUtcFromBakuDate(date, groupItem.attribute("text")); } else if ("az.kapitalbank.mbanking:id/transaction_amount" == groupItem.attribute("resource-id")) { auto amount = groupItem.attribute("text"); if (amount.startsWith("+")) { transaction.amount = amount.remove("+").remove("₼").replace(",", ".").trimmed().toFloat(); } } } if (transaction.amount > 0) { transactions.append(transaction); qDebug().noquote().nospace() << convertTransactionToString(transaction); } } } } else { qDebug() << "ListView is empty"; } return transactions; } TransactionInfo parseExpandedInfoHistoryBirbank(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 = transformBankDateTimeBirbank(findSubInfoBirbank(e, "1")); break; case 5: transaction.description = findSubInfoBirbank(e, "1"); parseTransferInfoBirbank(transaction.description, transaction); break; case 7: // Комиссия transaction.fee = findSubInfoBirbank(e, "1").remove(onlyDigitsRegex).toFloat(); break; default: // qDebug() << "Прочее:" << findSubInfo2(e); break; } } return transaction; } } // Рекурсивно обходим всех детей QDomNode child = root.firstChild(); while (!child.isNull()) { if (TransactionInfo info = parseExpandedInfoHistoryBirbank(child.toElement()); !info.bankTime.isEmpty()) { return info; } child = child.nextSibling(); } return {}; } TransactionInfo ScreenXmlBirbankParser::parseTransactionInfoHistory(int screenWidth, int screenHeight) { // 1. Распарсить описание транзакции TransactionInfo transaction = parseExpandedInfoHistoryBirbank(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; } TransactionInfo parseExpandedInfoBirbank(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; QDomElement infoElement = info.toElement(); QString index = infoElement.attribute("index"); QDomElement ee; switch (index.toInt()) { case 3: transaction.bankTime = transformBankDateTimeBirbank(findSubInfoBirbank(infoElement, "1")); break; case 4: ee = findSubElementBirbank(infoElement, "0"); ee = findSubElementBirbank(ee, "1"); transaction.phone = findSubInfoBirbank(ee, "0").remove(onlyDigitsRegex); break; case 5: transaction.name = findSubInfoBirbank(infoElement, "1"); break; case 6: transaction.bankName = findSubInfoBirbank(infoElement, "1"); break; case 9: transaction.fee = findSubInfoBirbank(infoElement, "1").remove(onlyDigitsAndDotRegex).toFloat(); break; case 10: ee = findSubElementBirbank(infoElement, "1"); transaction.bankTrExternalId = findSubInfoBirbank(ee, "1"); break; default: break; } } return transaction; } } // Рекурсивно обходим всех детей QDomNode child = root.firstChild(); while (!child.isNull()) { if (TransactionInfo info = parseExpandedInfoBirbank(child.toElement()); !info.bankTime.isEmpty()) { return info; } child = child.nextSibling(); } return {}; } TransactionInfo ScreenXmlBirbankParser::parseTransactionInfo(int screenWidth, int screenHeight) { // 1. Распарсить описание транзакции TransactionInfo transaction = parseExpandedInfoBirbank(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; } QList ScreenXmlBirbankParser::parseAccountsInfo() { const QDomNodeList nodes = m_xml.elementsByTagName("node"); QList accounts; for (int i = 0; i < nodes.count(); ++i) { const QDomElement el = nodes.at(i).toElement(); if (el.attribute("text").contains("₼")) { // Нашли сумму // el.attribute("text") - ₼ и трим auto amount = el.attribute("text").replace("₼", "").trimmed(); const QDomElement parent = el.parentNode().parentNode().toElement(); // Перебираем потомков QDomNode n = parent.firstChild().firstChild().firstChild().firstChild(); while (!n.isNull()) { const QDomElement e = n.toElement(); if (e.attribute("resource-id") == "az.kapitalbank.mbanking:id/tvNumber") { const QString account = e.attribute("text"); AccountInfo info; info.lastNumbers = account; info.amount = amount.toFloat(); info.description = account; info.updateTime = DateUtils::getUtcNow(); accounts.append(info); break; } n = n.nextSibling(); } } } return accounts; } void ScreenXmlBirbankParser::test() { // TransactionInfo info2 = parseExpandedInfo(m_xml); // qDebug().noquote().nospace() << convertTransactionToString(info2); // return; QFile file("assets/rshb/all_products.xml"); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Cannot open file"; } QDomDocument doc; if (!doc.setContent(&file)) { qDebug() << "Failed to parse XML"; } file.close(); qDebug() << "Start parsing XML:"; // Номер карты с последними цифрами QDomElement root = doc.documentElement(); parseAndSaveXml(doc.toString()); // for (const AccountInfo &info: parseAccountsInfo()) { // qDebug().noquote().nospace() << convertAccountToString(info); // } // TransactionInfo info = parseExpandedInfo(root); // qDebug().noquote().nospace() << convertTransactionToString(info); // ЕСПП }