#include "FileLogWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dao/EventDAO.h" #include "dao/GeneralLogDAO.h" #include "dao/SettingsDAO.h" #include "log/LogDetailWindow.h" static constexpr int COL_ID = 0; static constexpr int COL_TIME = 1; static constexpr int COL_LEVEL = 2; static constexpr int COL_SOURCE = 3; static constexpr int COL_MESSAGE = 4; FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) { setWindowTitle("Логи"); resize(1200, 650); m_table = new QTableWidget(this); m_table->setColumnCount(5); m_table->setHorizontalHeaderLabels({"#", "Время", "Уровень", "Источник", "Сообщение"}); m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); m_table->setSelectionBehavior(QAbstractItemView::SelectRows); m_table->setSelectionMode(QAbstractItemView::SingleSelection); m_table->verticalHeader()->hide(); m_table->horizontalHeader()->setStretchLastSection(true); m_table->setColumnWidth(COL_ID, 45); m_table->setColumnWidth(COL_TIME, 145); m_table->setColumnWidth(COL_LEVEL, 80); m_table->setColumnWidth(COL_SOURCE, 250); // Панель скриншота m_screenshotLabel = new QLabel(this); m_screenshotLabel->setAlignment(Qt::AlignCenter); m_screenshotLabel->setMinimumWidth(300); m_screenshotLabel->setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;"); m_screenshotLabel->setText("Выберите запись\nдля просмотра скриншота"); auto *splitter = new QSplitter(Qt::Horizontal, this); splitter->addWidget(m_table); splitter->addWidget(m_screenshotLabel); splitter->setStretchFactor(0, 3); splitter->setStretchFactor(1, 1); m_showAllCheck = new QCheckBox("Показать все (включая DEBUG/INFO)", this); m_prevBtn = new QPushButton("←", this); m_nextBtn = new QPushButton("→", this); m_pageLabel = new QLabel(this); m_pageLabel->setAlignment(Qt::AlignCenter); m_pageLabel->setMinimumWidth(140); auto *refreshBtn = new QPushButton("Обновить", this); m_sendBtn = new QPushButton("Отправить", this); m_sendBtn->setStyleSheet("background-color: #0088cc; color: white; border-radius: 4px; padding: 4px 12px;"); auto *paginationLayout = new QHBoxLayout; paginationLayout->addWidget(m_prevBtn); paginationLayout->addWidget(m_pageLabel); paginationLayout->addWidget(m_nextBtn); paginationLayout->addStretch(); paginationLayout->addWidget(m_showAllCheck); paginationLayout->addWidget(refreshBtn); paginationLayout->addWidget(m_sendBtn); auto *mainLayout = new QVBoxLayout(this); mainLayout->addWidget(splitter, 1); mainLayout->addLayout(paginationLayout); connect(m_prevBtn, &QPushButton::clicked, this, [this]() { if (m_page > 0) { --m_page; loadPage(); } }); connect(m_nextBtn, &QPushButton::clicked, this, [this]() { if (m_page < m_totalPages - 1) { ++m_page; loadPage(); } }); connect(refreshBtn, &QPushButton::clicked, this, [this]() { m_page = 0; loadPage(); }); connect(m_showAllCheck, &QCheckBox::toggled, this, [this]() { m_page = 0; loadPage(); }); connect(m_table, &QTableWidget::cellClicked, this, [this](int row, int) { showScreenshot(row); }); connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int) { if (row >= 0 && row < m_entries.size()) { auto *detail = new LogDetailWindow(m_entries[row], this); detail->setAttribute(Qt::WA_DeleteOnClose); detail->show(); } }); connect(m_sendBtn, &QPushButton::clicked, this, &FileLogWindow::sendLogToTelegram); loadPage(); } QStringList FileLogWindow::currentLevelFilter() const { if (m_showAllCheck->isChecked()) return {}; return {"WARNING", "CRITICAL", "FATAL"}; } void FileLogWindow::loadPage() { const QStringList levels = currentLevelFilter(); const int total = GeneralLogDAO::getTotalCount(levels); m_totalPages = qMax(1, (total + m_pageSize - 1) / m_pageSize); m_page = qBound(0, m_page, m_totalPages - 1); m_entries = GeneralLogDAO::getLogs(m_page, m_pageSize, levels); m_table->setRowCount(static_cast(m_entries.size())); for (int i = 0; i < m_entries.size(); ++i) { const auto &e = m_entries[i]; m_table->setItem(i, COL_ID, new QTableWidgetItem(QString::number(e.id))); m_table->setItem(i, COL_TIME, new QTableWidgetItem( e.timestamp.toLocalTime().toString("dd.MM.yy HH:mm:ss"))); m_table->setItem(i, COL_LEVEL, new QTableWidgetItem(e.level)); m_table->setItem(i, COL_SOURCE, new QTableWidgetItem(e.source)); m_table->setItem(i, COL_MESSAGE, new QTableWidgetItem(e.message)); } updatePagination(); } void FileLogWindow::showScreenshot(int row) { if (row < 0 || row >= m_entries.size()) return; const QByteArray &data = m_entries[row].screenshot; if (data.isEmpty()) { m_screenshotLabel->setText("Нет скриншота"); return; } QPixmap pixmap; pixmap.loadFromData(data); if (pixmap.isNull()) { m_screenshotLabel->setText("Ошибка загрузки"); return; } m_screenshotLabel->setPixmap(pixmap.scaled( m_screenshotLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } void FileLogWindow::updatePagination() { m_pageLabel->setText(QString("Страница %1 из %2").arg(m_page + 1).arg(m_totalPages)); m_prevBtn->setEnabled(m_page > 0); m_nextBtn->setEnabled(m_page < m_totalPages - 1); } void FileLogWindow::sendLogToTelegram() { qDebug() << "[sendLogToTelegram] step 1: checking log file"; const QString logPath = QCoreApplication::applicationDirPath() + "/application.log"; if (!QFile::exists(logPath)) { QMessageBox::warning(this, "Ошибка", "Файл application.log не найден"); return; } m_sendBtn->setEnabled(false); m_sendBtn->setText("Подготовка..."); qDebug() << "[sendLogToTelegram] step 2: creating tmp dir"; // Собираем временную папку для архива const QString tmpDir = QDir::tempPath() + "/arc_logs_export"; QDir().mkpath(tmpDir); // 1. Копируем application.log QFile::remove(tmpDir + "/application.log"); QFile::copy(logPath, tmpDir + "/application.log"); qDebug() << "[sendLogToTelegram] step 3: exporting errors"; // 2. Экспортируем events с ошибками в errors.txt const QList errors = EventDAO::getAllEvents(EventStatus::Error); if (!errors.isEmpty()) { QFile errFile(tmpDir + "/errors.txt"); if (errFile.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream out(&errFile); for (const EventInfo &e : errors) { out << "--- Event #" << e.id << " ---\n" << "Type: " << eventTypeToString(e.type) << "\n" << "Bank: " << e.bankName << "\n" << "Device: " << e.deviceId << "\n" << "Amount: " << e.amount << "\n" << "Phone: " << e.phone << "\n" << "Comment: " << e.comment << "\n" << "Time: " << e.timestamp.toString(Qt::ISODate) << "\n" << "JSON: " << e.json << "\n\n"; } errFile.close(); } } qDebug() << "[sendLogToTelegram] step 4: exporting event screenshots"; // 3. Экспортируем скриншоты ошибок из events QDir().mkpath(tmpDir + "/screenshots"); int screenshotCount = 0; for (const EventInfo &e : errors) { const QByteArray screenshot = EventDAO::getScreenshot(e.id); if (screenshot.isEmpty()) continue; const QString fileName = QString("screenshots/%1_%2_%3.png") .arg(e.id) .arg(eventTypeToString(e.type)) .arg(e.timestamp.toString("yyyyMMdd_HHmmss")); QFile imgFile(tmpDir + "/" + fileName); if (imgFile.open(QIODevice::WriteOnly)) { imgFile.write(screenshot); imgFile.close(); ++screenshotCount; } } qDebug() << "[sendLogToTelegram] step 5: exporting log screenshots"; // 3.5. Экспортируем скриншоты из general_logs (ошибки скриптов) const QList logErrors = GeneralLogDAO::getLogs(0, 100, {"WARNING", "CRITICAL", "FATAL"}); for (const GeneralLogEntry &gl : logErrors) { if (gl.screenshot.isEmpty()) continue; const QString fileName = QString("screenshots/log_%1_%2.png") .arg(gl.id) .arg(gl.timestamp.toString("yyyyMMdd_HHmmss")); QFile imgFile(tmpDir + "/" + fileName); if (imgFile.open(QIODevice::WriteOnly)) { imgFile.write(gl.screenshot); imgFile.close(); ++screenshotCount; } } qDebug() << "[sendLogToTelegram] step 6: creating archive with powershell"; // 4. Создаём архив #ifdef Q_OS_WIN const QString archivePath = QDir::tempPath() + "/arc_logs.zip"; QFile::remove(archivePath); QProcess archiver; archiver.setWorkingDirectory(tmpDir); archiver.start("powershell", QStringList() << "-NoProfile" << "-Command" << QString("Compress-Archive -Path '%1/*' -DestinationPath '%2' -Force").arg(tmpDir, archivePath)); if (!archiver.waitForFinished(30000)) { qWarning() << "[sendLogToTelegram] powershell archiver timeout/error:" << archiver.errorString(); } qDebug() << "[sendLogToTelegram] step 6.1: archiver exit code:" << archiver.exitCode() << "stdout:" << archiver.readAllStandardOutput() << "stderr:" << archiver.readAllStandardError(); #else const QString archivePath = QDir::tempPath() + "/arc_logs.zip"; QFile::remove(archivePath); QProcess archiver; archiver.setWorkingDirectory(tmpDir); archiver.start("zip", QStringList() << "-r" << archivePath << "."); archiver.waitForFinished(30000); #endif // Чистим временную папку QDir(tmpDir).removeRecursively(); qDebug() << "[sendLogToTelegram] step 7: archive exists:" << QFile::exists(archivePath) << "path:" << archivePath; if (!QFile::exists(archivePath)) { QMessageBox::warning(this, "Ошибка", "Не удалось создать архив"); m_sendBtn->setEnabled(true); m_sendBtn->setText("Отправить"); return; } m_sendBtn->setText("Отправка..."); QFile *zipFile = new QFile(archivePath); if (!zipFile->open(QIODevice::ReadOnly)) { QMessageBox::warning(this, "Ошибка", "Не удалось открыть архив"); delete zipFile; m_sendBtn->setEnabled(true); m_sendBtn->setText("Отправить"); return; } // Проверяем поддержку SSL if (!QSslSocket::supportsSsl()) { qWarning() << "[sendLogToTelegram] SSL not supported! Build:" << QSslSocket::sslLibraryBuildVersionString() << "Runtime:" << QSslSocket::sslLibraryVersionString(); QMessageBox::warning(this, "Ошибка", "SSL не поддерживается.\nНевозможно отправить логи через HTTPS."); delete zipFile; m_sendBtn->setEnabled(true); m_sendBtn->setText("Отправить"); return; } qDebug() << "[sendLogToTelegram] SSL OK, sending..."; // Telegram Bot API const QString botToken = "8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8"; const QString chatId = "-5177086220"; const QString desktopId = SettingsDAO::get("desktop_id"); const QString caption = "Logs from desktop: " + desktopId + "\nErrors: " + QString::number(errors.size()) + "\nScreenshots: " + QString::number(screenshotCount); const QUrl url("https://api.telegram.org/bot" + botToken + "/sendDocument"); auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); QHttpPart chatPart; chatPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="chat_id")"); chatPart.setBody(chatId.toUtf8()); multiPart->append(chatPart); QHttpPart captionPart; captionPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="caption")"); captionPart.setBody(caption.toUtf8()); multiPart->append(captionPart); QHttpPart filePart; filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip"); filePart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="document"; filename="arc_logs.zip")"); filePart.setBodyDevice(zipFile); zipFile->setParent(multiPart); multiPart->append(filePart); qDebug() << "[sendLogToTelegram] step 8: creating network request"; auto *manager = new QNetworkAccessManager(this); // Принудительно используем Schannel (нативный Windows TLS) вместо OpenSSL QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); sslConfig.setBackendConfigurationOption("SslProtocol", "TlsV1_2OrLater"); QNetworkRequest req(url); req.setSslConfiguration(sslConfig); qDebug() << "[sendLogToTelegram] step 9: posting to telegram..." << "SSL backend:" << QSslSocket::activeBackend(); QNetworkReply *reply = manager->post(req, multiPart); qDebug() << "[sendLogToTelegram] step 10: post sent, waiting for reply"; multiPart->setParent(reply); connect(reply, &QNetworkReply::finished, this, [this, reply, manager, archivePath]() { reply->deleteLater(); manager->deleteLater(); QFile::remove(archivePath); m_sendBtn->setEnabled(true); m_sendBtn->setText("Отправить"); if (reply->error() != QNetworkReply::NoError) { QMessageBox::warning(this, "Ошибка", "Не удалось отправить: " + reply->errorString()); return; } QMessageBox::information(this, "Готово", "Логи отправлены в Telegram"); }); }