@ -1,24 +1,24 @@
# include "FileLogWindow.h"
# include <QCoreApplication>
# include <QDateTime>
# include <QDir>
# include <QFile>
# include <QFileInfo>
# include <QHBoxLayout>
# include <QHeaderView>
# include <QHttpMultiPart>
# include <QMessageBox>
# include <QNetworkAccessManager>
# include <QNetworkReply>
# include <QSslSocket>
# include <QMetaObject>
# include <QSettings>
# include <QProcess>
# include <QTemporaryFile>
# include <QPixmap>
# include <QVBoxLayout>
# include "d ao/EventDAO .h"
# include "d ump/TaskDumpRecorder .h"
# include "dao/GeneralLogDAO.h"
# include "dao/SettingsDAO.h"
# include "log/LogDetailWindow.h"
# include "net/NetworkService.h"
static constexpr int COL_ID = 0 ;
static constexpr int COL_TIME = 1 ;
@ -105,7 +105,7 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
detail - > show ( ) ;
}
} ) ;
connect ( m_sendBtn , & QPushButton : : clicked , this , & FileLogWindow : : sendLog ToTelegram ) ;
connect ( m_sendBtn , & QPushButton : : clicked , this , & FileLogWindow : : sendLog sToBackend ) ;
loadPage ( ) ;
}
@ -163,114 +163,84 @@ void FileLogWindow::updatePagination() {
m_nextBtn - > setEnabled ( m_page < m_totalPages - 1 ) ;
}
void FileLogWindow : : sendLog ToTelegram ( ) {
qDebug ( ) < < " [sendLog ToTelegram ] step 1: checking log file" ;
void FileLogWindow : : sendLog sToBackend ( ) {
qDebug ( ) < < " [sendLog sToBackend ] step 1: checking log file" ;
const QString logPath = QCoreApplication : : applicationDirPath ( ) + " /application.log " ;
if ( ! QFile : : exists ( logPath ) ) {
QMessageBox : : warning ( this , " Ошибка " , " Файл application.log не найден " ) ;
return ;
}
// Резолвим путь к БД из config.ini (как DatabaseManager): [db]/dbPath,
// с разворотом '~' в домашнюю директорию и приведением к абсолютному пути
// относительно applicationDirPath, чтобы не зависеть от текущего CWD.
const QSettings settings ( " config.ini " , QSettings : : IniFormat ) ;
QString dbPath = settings . value ( " db/dbPath " ) . toString ( ) ;
if ( dbPath . startsWith ( " ~ " ) ) {
dbPath . replace ( 0 , 1 , QDir : : homePath ( ) ) ;
}
if ( ! dbPath . isEmpty ( ) & & QFileInfo ( dbPath ) . isRelative ( ) ) {
dbPath = QDir ( QCoreApplication : : applicationDirPath ( ) ) . absoluteFilePath ( dbPath ) ;
}
if ( dbPath . isEmpty ( ) | | ! QFile : : exists ( dbPath ) ) {
QMessageBox : : warning ( this , " Ошибка " ,
" Файл БД не найден: " + ( dbPath . isEmpty ( ) ? QStringLiteral ( " <пусто> " ) : dbPath ) ) ;
return ;
}
NetworkService * svc = TaskDumpRecorder : : networkService ( ) ;
if ( ! svc ) {
QMessageBox : : warning ( this , " Ошибка " ,
" NetworkService не инициализирован — отправка на бэкенд недоступна. " ) ;
return ;
}
m_sendBtn - > setEnabled ( false ) ;
m_sendBtn - > setText ( " Подготовка... " ) ;
qDebug ( ) < < " [sendLogToTelegram] step 2: creating tmp dir " ;
// Собираем временную папку для архива
qDebug ( ) < < " [sendLogsToBackend] step 2: creating tmp dir " ;
const QString tmpDir = QDir : : tempPath ( ) + " /arc_logs_export " ;
QDir ( tmpDir ) . removeRecursively ( ) ;
QDir ( ) . mkpath ( tmpDir ) ;
// 1. Копируем application.log
QFile : : remove ( tmpDir + " /application.log " ) ;
// 1. 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 ( ) ;
// 2. БД + WAL/SHM-сайдкары (если есть) — иначе при включённом WAL
// снимок будет рассогласованным.
const QString dbBaseName = QFileInfo ( dbPath ) . fileName ( ) ;
QFile : : copy ( dbPath , tmpDir + " / " + dbBaseName ) ;
for ( const QString & suffix : { QStringLiteral ( " -wal " ) , QStringLiteral ( " -shm " ) } ) {
const QString sidecar = dbPath + suffix ;
if ( QFile : : exists ( sidecar ) ) {
QFile : : copy ( sidecar , tmpDir + " / " + dbBaseName + suffix ) ;
}
}
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
qDebug ( ) < < " [sendLogsToBackend] step 3: creating archive " ;
const QString archivePath = QDir : : tempPath ( ) + " /arc_logs.zip " ;
QFile : : remove ( archivePath ) ;
# ifdef Q_OS_WIN
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 ( ) < < " [sendLog ToTelegram ] powershell archiver timeout/error:" < < archiver . errorString ( ) ;
qWarning ( ) < < " [sendLogsToBackend] powershell archiver timeout/error: " < < archiver . errorString ( ) ;
}
qDebug ( ) < < " [sendLog ToTelegram] step 6.1: archiver exit code:" < < archiver . exitCode ( )
qDebug ( ) < < " [sendLogsToBackend] 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 ) ;
@ -278,88 +248,46 @@ void FileLogWindow::sendLogToTelegram() {
return ;
}
m_sendBtn - > setText ( " Отправка... " ) ;
QFile * zipFile = new QFile ( archivePath ) ;
if ( ! zipFile - > open ( QIODevice : : ReadOnly ) ) {
QFile zipFile ( archivePath ) ;
if ( ! zipFile . open ( QIODevice : : ReadOnly ) ) {
QMessageBox : : warning ( this , " Ошибка " , " Н е удалось открыть архив" ) ;
delete zipFile ;
QFile : : remove ( archivePath ) ;
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
+ " \n Errors: " + QString : : number ( errors . size ( ) )
+ " \n Screenshots: " + 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 ( ) ;
const QByteArray archiveBytes = zipFile . readAll ( ) ;
zipFile . close ( ) ;
QFile : : remove ( archivePath ) ;
if ( archiveBytes . isEmpty ( ) ) {
QMessageBox : : warning ( this , " Ошибка " , " Архив пуст — отправлять нечего " ) ;
m_sendBtn - > setEnabled ( true ) ;
m_sendBtn - > setText ( " Отправить " ) ;
if ( reply - > error ( ) ! = QNetworkReply : : NoError ) {
QMessageBox : : warning ( this , " Ошибка " , " Н е удалось отправить: " + reply - > errorString ( ) ) ;
return ;
}
QMessageBox : : information ( this , " Готово " , " Логи отправлены в Telegram " ) ;
} ) ;
const QString desktopId = SettingsDAO : : get ( " desktop_id " ) ;
const QString text = QStringLiteral ( " kind: manual_logs \n desktop: %1 \n time: %2 " )
. arg ( desktopId , QDateTime : : currentDateTime ( ) . toString ( Qt : : ISODate ) ) ;
qDebug ( ) < < " [sendLogsToBackend] dispatching to NetworkService::postMonitoringDump "
< < " archive.size= " < < archiveBytes . size ( ) ;
const bool dispatched = QMetaObject : : invokeMethod ( svc , " postMonitoringDump " ,
Qt : : QueuedConnection ,
Q_ARG ( QString , text ) ,
Q_ARG ( QByteArray , archiveBytes ) ,
Q_ARG ( QByteArray , QByteArray ( ) ) ) ;
m_sendBtn - > setEnabled ( true ) ;
m_sendBtn - > setText ( " Отправить " ) ;
if ( ! dispatched ) {
QMessageBox : : warning ( this , " Ошибка " ,
" Н е удалось поставить отправку в очередь NetworkService." ) ;
return ;
}
QMessageBox : : information ( this , " Готово " ,
" Логи поставлены в очередь на отправку на бэкенд. " ) ;
}