Files
TTW_Bot/webservernotify.cpp
T
2026-01-26 22:26:19 +03:00

508 lines
17 KiB
C++

#include "webservernotify.h"
#include <QDateTime>
#include <QCoreApplication>
#include <QDir>
#include <QDebug>
#include <QJsonDocument>
#include <QFileInfo>
HttpServer::HttpServer(QObject *parent)
: QObject(parent)
, m_server(nullptr)
, m_pageBackgroundColor("transparent")
{
}
HttpServer::~HttpServer()
{
stop();
}
bool HttpServer::start(quint16 port)
{
if (m_server) {
stop();
}
m_server = new QTcpServer(this);
if (!m_server->listen(QHostAddress::Any, port)) {
delete m_server;
m_server = nullptr;
emit serverStarted(false);
return false;
}
connect(m_server, &QTcpServer::newConnection, this, &HttpServer::onNewConnection);
emit serverStarted(true);
return true;
}
void HttpServer::stop()
{
if (m_server) {
m_server->close();
m_server->deleteLater();
m_server = nullptr;
}
// Закрываем всех клиентов
for (auto client : m_clients) {
if (client->state() == QAbstractSocket::ConnectedState) {
client->close();
}
client->deleteLater();
}
m_clients.clear();
}
quint16 HttpServer::port() const
{
return m_server ? m_server->serverPort() : 0;
}
void HttpServer::addNotification(const Notification &notification)
{
cleanOldMessages();
// Добавляем временную метку
Notification notif = notification;
notif.timestamp = QDateTime::currentSecsSinceEpoch();
// Обновляем цвет фона страницы из уведомления
if (!notification.pageBackgroundColor.isEmpty()) {
m_pageBackgroundColor = notification.pageBackgroundColor;
}
m_notifications.append(notif);
// Обновляем всех подключенных клиентов
for (auto client : m_clients) {
if (client->state() == QAbstractSocket::ConnectedState) {
// Отправляем JavaScript для обновления страницы
QString js = "<script>window.location.reload();</script>";
sendResponse(client, "text/html", js);
}
}
}
void HttpServer::clearNotifications()
{
m_notifications.clear();
}
void HttpServer::cleanOldMessages()
{
qint64 currentTime = QDateTime::currentSecsSinceEpoch();
for (int i = m_notifications.size() - 1; i >= 0; --i) {
if (currentTime - m_notifications[i].timestamp >= m_notifications[i].duration) {
m_notifications.removeAt(i);
}
}
}
void HttpServer::onNewConnection()
{
while (m_server->hasPendingConnections()) {
QTcpSocket *socket = m_server->nextPendingConnection();
m_clients.append(socket);
// Устанавливаем keep-alive
connect(socket, &QTcpSocket::readyRead, this, &HttpServer::readClient);
connect(socket, &QTcpSocket::disconnected, this, &HttpServer::discardClient);
}
}
void HttpServer::readClient()
{
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
if (!socket) return;
QByteArray requestData = socket->readAll();
QString request = QString::fromUtf8(requestData);
if (!request.isEmpty()) {
processRequest(socket, request);
}
}
void HttpServer::processRequest(QTcpSocket *socket, const QString &request)
{
QStringList lines = request.split("\r\n");
if (lines.isEmpty()) return;
QString firstLine = lines[0];
QStringList parts = firstLine.split(" ");
if (parts.size() < 2) return;
QString method = parts[0];
QString path = parts[1];
if (method == "GET") {
if (path == "/" || path == "/index.html") {
sendHtmlPage(socket);
} else if (path == "/messages") {
sendJSONMessages(socket);
} else if (path.startsWith("/sounds/")) {
QString filePath = path.mid(1); // Убираем первый слэш
serveStaticFile(socket, filePath);
} else if (path.startsWith("/imgs/")) {
QString filePath = path.mid(1);
serveStaticFile(socket, filePath);
} else if (path.startsWith("/fonts/")) {
QString filePath = path.mid(1);
serveStaticFile(socket, filePath);
} else if (path == "/clear") {
clearNotifications();
sendResponse(socket, "text/html", "<html><body>Notifications cleared</body></html>");
} else {
// Игнорируем favicon и другие запросы
if (!path.contains("favicon.ico") && !path.contains(".well-known")) {
}
sendResponse(socket, "text/html", "", 404);
}
}
}
void HttpServer::sendHtmlPage(QTcpSocket *socket)
{
QString html = generateHTML();
sendResponse(socket, "text/html", html);
}
void HttpServer::sendJSONMessages(QTcpSocket *socket)
{
QString json = generateJSON();
// Убедимся, что это валидный JSON
QJsonParseError parseError;
QJsonDocument::fromJson(json.toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Ошибка парсинга JSON:" << parseError.errorString();
json = "[]"; // Отправляем пустой массив при ошибке
}
sendResponse(socket, "application/json; charset=utf-8", json);
}
void HttpServer::serveStaticFile(QTcpSocket *socket, const QString &filePath)
{
QString fullPath = QCoreApplication::applicationDirPath() + "/" + filePath;
QFile file(fullPath);
if (file.exists() && file.open(QIODevice::ReadOnly)) {
QString mimeType = getMimeType(filePath);
QByteArray content = file.readAll();
file.close();
QString response = QString(
"HTTP/1.1 200 OK\r\n"
"Content-Type: %1\r\n"
"Content-Length: %2\r\n"
"Cache-Control: no-cache\r\n"
"Connection: close\r\n"
"\r\n")
.arg(mimeType)
.arg(content.size());
socket->write(response.toUtf8());
socket->write(content);
socket->flush();
} else {
sendResponse(socket, "text/html",
"<html><body><h1>404 Not Found</h1><p>" + filePath + "</p></body></html>",
404);
}
}
QString HttpServer::generateHTML()
{
QString html =
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
"<meta charset='UTF-8'>\n"
"<title>Web Notifications</title>\n"
"<style>\n"
"body { background: %1; margin: 0; padding: 0; }\n" // Используем цвет фона страницы
"#messages { position: fixed; top: 20px; right: 20px; width: 400px; }\n"
".notification { \n"
" margin: 10px 0; \n"
" border-radius: 10px; \n"
" padding: 15px; \n"
" max-width: 100%; \n"
" box-shadow: 0 4px 15px rgba(0,0,0,0.2);\n"
" animation: fadeIn 0.3s ease-in;\n"
" opacity: 1;\n"
" transition: opacity 1s ease-out;\n"
"}\n"
".notification.fade-out {\n"
" opacity: 0 !important;\n"
"}\n"
".nick { margin: 0 0 5px 0; padding: 0; }\n"
".text { margin: 0; padding: 0; }\n"
"@keyframes fadeIn {\n"
" from { opacity: 0; transform: translateY(-20px); }\n"
" to { opacity: 1; transform: translateY(0); }\n"
"}\n"
"</style>\n"
"</head>\n"
"<body>\n"
"<div id='messages'></div>\n"
"<script>\n"
"let notifications = new Map(); // Храним активные уведомления по timestamp\n"
"let lastSoundTime = 0;\n"
"\n"
"function loadMessages() {\n"
" fetch('/messages')\n"
" .then(response => response.json())\n"
" .then(data => {\n"
" updateNotifications(data);\n"
" })\n"
" .catch(error => console.error('Error:', error));\n"
"}\n"
"\n"
"function updateNotifications(messages) {\n"
" const container = document.getElementById('messages');\n"
" \n"
" // Проходим по всем полученным сообщениям\n"
" messages.forEach(msg => {\n"
" const msgId = 'msg-' + msg.timestamp;\n"
" \n"
" // Если уведомление уже есть, пропускаем\n"
" if (notifications.has(msgId)) {\n"
" return;\n"
" }\n"
" \n"
" // Создаем новое уведомление\n"
" const div = document.createElement('div');\n"
" div.className = 'notification';\n"
" div.id = msgId;\n"
" \n"
" // Применяем стили из настроек\n"
" div.style.backgroundColor = msg.color || '#4CAF50';\n"
" div.style.border = (msg.borderSize || 2) + 'px solid ' + (msg.borderColor || '#2E7D32');\n"
" div.style.padding = '15px';\n"
" \n"
" // Формируем содержимое\n"
" let content = '';\n"
" \n"
" // Изображение\n"
" if (msg.url && msg.url !== '') {\n"
" content += '<img src=\"' + msg.url + '\" style=\"max-width: 100%; height: auto; border-radius: 5px; margin-bottom: 10px;\">';\n"
" }\n"
" \n"
" // Заголовок\n"
" content += '<div class=\"nick\" style=\"'\n"
" + 'color: ' + (msg.titleColor || '#FFFFFF') + ';'\n"
" + 'font-family: \"' + (msg.titleFamily || 'Arial') + '\", sans-serif;'\n"
" + 'font-size: ' + (msg.titleSize || 20) + 'px;'\n"
" + 'font-weight: bold;'\n"
" + 'text-shadow: 1px 1px 2px rgba(0,0,0,0.5);'\n"
" + '\">'\n"
" + msg.nickname\n"
" + '</div>';\n"
" \n"
" // Текст\n"
" content += '<div class=\"text\" style=\"'\n"
" + 'color: ' + (msg.contentColor || '#F5F5F5') + ';'\n"
" + 'font-family: \"' + (msg.contentFamily || 'Arial') + '\", sans-serif;'\n"
" + 'font-size: ' + (msg.contentSize || 16) + 'px;'\n"
" + '\">'\n"
" + msg.content\n"
" + '</div>';\n"
" \n"
" div.innerHTML = content;\n"
" container.appendChild(div);\n"
" \n"
" // Добавляем в карту\n"
" notifications.set(msgId, {\n"
" element: div,\n"
" timestamp: msg.timestamp,\n"
" duration: msg.duration || 10\n"
" });\n"
" \n"
" // Проигрываем звук\n"
" if (msg.sound && msg.sound !== '') {\n"
" playSound(msg.sound);\n"
" }\n"
" \n"
" // Запускаем таймер удаления\n"
" setTimeout(() => {\n"
" removeNotification(msgId);\n"
" }, (msg.duration || 10) * 1000);\n"
" });\n"
"}\n"
"\n"
"function removeNotification(id) {\n"
" if (notifications.has(id)) {\n"
" const notification = notifications.get(id);\n"
" notification.element.classList.add('fade-out');\n"
" \n"
" // Удаляем после анимации\n"
" setTimeout(() => {\n"
" if (notification.element.parentNode) {\n"
" notification.element.parentNode.removeChild(notification.element);\n"
" }\n"
" notifications.delete(id);\n"
" }, 1000);\n"
" }\n"
"}\n"
"\n"
"function playSound(soundUrl) {\n"
" if (!soundUrl) return;\n"
" \n"
" const audio = new Audio(soundUrl);\n"
" audio.volume = 0.5;\n"
" audio.play().catch(e => console.log('Audio error:', e));\n"
"}\n"
"\n"
"// Загружаем при старте\n"
"loadMessages();\n"
"\n"
"// Обновляем каждые 2 секунды (не каждый раз)\n"
"setInterval(loadMessages, 2000);\n"
"\n"
"// Обновляем при возврате на вкладку\n"
"document.addEventListener('visibilitychange', () => {\n"
" if (!document.hidden) {\n"
" loadMessages();\n"
" }\n"
"});\n"
"</script>\n"
"</body>\n"
"</html>";
return html.arg(m_pageBackgroundColor); // Подставляем цвет фона
}
QString HttpServer::generateJSON()
{
cleanOldMessages();
QJsonArray jsonArray;
for (const Notification &notif : m_notifications) {
QJsonObject obj;
obj["nickname"] = notif.nickname;
// Исправляем пути - добавляем слеш в начале
if (!notif.url.isEmpty()) {
obj["url"] = notif.url.startsWith("/") ? notif.url : "/" + notif.url;
} else {
obj["url"] = "";
}
obj["content"] = notif.content;
obj["timestamp"] = notif.timestamp;
// Исправляем путь к звуку
if (!notif.soundURL.isEmpty()) {
obj["sound"] = notif.soundURL.startsWith("/") ? notif.soundURL : "/" + notif.soundURL;
} else {
obj["sound"] = "";
}
obj["duration"] = notif.duration;
obj["color"] = notif.blockColor;
obj["borderColor"] = notif.borderColor;
obj["borderSize"] = notif.borderSize;
obj["titleColor"] = notif.titleColor;
obj["titleFamily"] = notif.titleFamily;
obj["titleSize"] = notif.titleSize;
obj["contentColor"] = notif.contentColor;
obj["contentFamily"] = notif.contentFamily;
obj["contentSize"] = notif.contentSize;
jsonArray.append(obj);
}
QJsonDocument doc(jsonArray);
QString jsonStr = QString::fromUtf8(doc.toJson());
return jsonStr;
}
void HttpServer::sendResponse(QTcpSocket *socket, const QString &contentType, const QString &content, int statusCode)
{
QString statusText;
switch(statusCode) {
case 200: statusText = "OK"; break;
case 404: statusText = "Not Found"; break;
default: statusText = "OK"; break;
}
QByteArray responseData = QString(
"HTTP/1.1 %1 %2\r\n"
"Content-Type: %3\r\n"
"Content-Length: %4\r\n"
"Cache-Control: no-cache, no-store, must-revalidate\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Connection: keep-alive\r\n"
"\r\n"
"%5")
.arg(statusCode)
.arg(statusText)
.arg(contentType)
.arg(content.toUtf8().size())
.arg(content)
.toUtf8();
socket->write(responseData);
// Не закрываем соединение сразу для keep-alive
if (contentType == "text/html" || contentType.startsWith("application/json")) {
// Для HTML и JSON оставляем соединение открытым
socket->flush();
} else {
// Для статических файлов закрываем после отправки
socket->flush();
socket->close();
}
}
QString HttpServer::getMimeType(const QString &filePath)
{
if (filePath.endsWith(".html")) return "text/html";
if (filePath.endsWith(".css")) return "text/css";
if (filePath.endsWith(".js")) return "application/javascript";
if (filePath.endsWith(".png")) return "image/png";
if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) return "image/jpeg";
if (filePath.endsWith(".gif")) return "image/gif";
if (filePath.endsWith(".svg")) return "image/svg+xml";
if (filePath.endsWith(".ico")) return "image/x-icon";
if (filePath.endsWith(".ttf")) return "font/ttf";
if (filePath.endsWith(".otf")) return "font/otf";
if (filePath.endsWith(".woff")) return "font/woff";
if (filePath.endsWith(".woff2")) return "font/woff2";
if (filePath.endsWith(".mp3")) return "audio/mpeg";
if (filePath.endsWith(".wav")) return "audio/wav";
if (filePath.endsWith(".ogg")) return "audio/ogg";
return "text/plain";
}
void HttpServer::discardClient()
{
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
if (socket) {
m_clients.removeOne(socket);
socket->deleteLater();
}
}