508 lines
17 KiB
C++
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 ¬ification)
|
|
{
|
|
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 ¬if : 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();
|
|
}
|
|
}
|