366 lines
14 KiB
C++
366 lines
14 KiB
C++
#include "FileLogWindow.h"
|
||
|
||
#include <QCoreApplication>
|
||
#include <QDir>
|
||
#include <QFile>
|
||
#include <QHBoxLayout>
|
||
#include <QHeaderView>
|
||
#include <QHttpMultiPart>
|
||
#include <QMessageBox>
|
||
#include <QNetworkAccessManager>
|
||
#include <QNetworkReply>
|
||
#include <QSslSocket>
|
||
#include <QProcess>
|
||
#include <QTemporaryFile>
|
||
#include <QPixmap>
|
||
#include <QVBoxLayout>
|
||
|
||
#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<int>(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<EventInfo> 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<GeneralLogEntry> 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");
|
||
});
|
||
}
|