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

740 lines
25 KiB
C++

#include "webserverchat.h"
#include "qcolor.h"
#include <QCoreApplication>
#include <QDir>
#include <QDebug>
#include <QJsonDocument>
#include <QFileInfo>
#include <QDateTime>
HttpServerChat::HttpServerChat(const QStringList &fontList, int port, const QString &backgroundColor, QObject *parent)
: QObject(parent)
, m_server(nullptr)
, m_fontList(fontList)
, m_backgroundColor(backgroundColor)
, m_deleteByTime(true) // По умолчанию удаление по времени
, m_maxMsgCount(100) // Значение по умолчанию
{
m_server = new QTcpServer(this);
m_server->listen(QHostAddress::Any, port);
connect(m_server, &QTcpServer::newConnection, this, &HttpServerChat::onNewConnection);
m_currentStyle.freez = m_isFreez;
}
HttpServerChat::~HttpServerChat()
{
stop();
}
bool HttpServerChat::start()
{
if (m_server->isListening()) {
emit serverStarted(true);
return true;
}
if (!m_server->listen(QHostAddress::Any, m_server->serverPort())) {
emit serverStarted(false);
return false;
}
emit serverStarted(true);
return true;
}
void HttpServerChat::stop()
{
if (m_server) {
m_server->close();
}
for (auto client : m_clients) {
if (client->state() == QAbstractSocket::ConnectedState) {
client->close();
}
client->deleteLater();
}
m_clients.clear();
}
quint16 HttpServerChat::port() const
{
return m_server ? m_server->serverPort() : 0;
}
void HttpServerChat::addMessage(const StyleChat &style)
{
m_currentStyle = style;
ChatMessage msg;
msg.nickname = style.nick;
msg.content = style.context;
msg.timestamp = QDateTime::currentSecsSinceEpoch();
msg.timeMsg = style.timeMsg;
msg.freez = style.freez;
msg.transparency = style.transparency;
// Копируем стилевые параметры
msg.blockColor = style.blockColor;
msg.borderColor = style.borderColor;
msg.fontColor = style.fontColor;
msg.fontFamily = style.fontFamily;
msg.fontSize = style.fontSize;
msg.borderSize = style.borderSize;
msg.padding = style.padding;
if (!m_deleteByTime) {
// Режим удаления по количеству
// Проверяем, не превышен ли лимит
if (m_messages.size() >= m_maxMsgCount) {
// Ищем незамороженное сообщение для удаления
bool removed = false;
for (int i = 0; i < m_messages.size(); ++i) {
if (!m_messages[i].freez) {
m_messages.removeAt(i);
removed = true;
break;
}
}
// Если все сообщения заморожены и лимит превышен,
// не добавляем новое сообщение
if (!removed) {
return;
}
}
}
m_messages.append(msg);
// Обновляем всех подключенных клиентов
for (auto client : m_clients) {
if (client->state() == QAbstractSocket::ConnectedState) {
// Отправляем JavaScript для обновления страницы
QString js = "<script>window.location.reload();</script>";
sendResponse(client, "text/html", js);
}
}
}
void HttpServerChat::activeServer(bool enabled)
{
if (enabled) {
if (!m_server->isListening()) {
m_server->listen(QHostAddress::Any, m_server->serverPort());
}
} else {
m_server->close();
}
}
void HttpServerChat::changeBackground(const QString &color)
{
m_backgroundColor = color;
}
void HttpServerChat::onNewConnection()
{
while (m_server->hasPendingConnections()) {
QTcpSocket *socket = m_server->nextPendingConnection();
m_clients.append(socket);
connect(socket, &QTcpSocket::readyRead, this, &HttpServerChat::readClient);
connect(socket, &QTcpSocket::disconnected, this, &HttpServerChat::discardClient);
}
}
void HttpServerChat::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 HttpServerChat::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];
cleanupOldMessages();
if (method == "GET") {
if (path == "/" || path == "/index.html") {
sendHtmlPage(socket);
} else if (path == "/messages") {
sendJSONMessages(socket);
} else if (path.startsWith("/fonts/")) {
QString filePath = path.mid(1); // Убираем первый слэш
serveStaticFile(socket, filePath);
} else {
// Игнорируем favicon и другие запросы
if (!path.contains("favicon.ico") && !path.contains(".well-known")) {
}
sendResponse(socket, "text/html", "", 404);
}
}
}
void HttpServerChat::sendHtmlPage(QTcpSocket *socket)
{
QString html = generateHTML();
sendResponse(socket, "text/html", html);
}
void HttpServerChat::sendJSONMessages(QTcpSocket *socket)
{
QString json = generateJSON();
sendResponse(socket, "application/json; charset=utf-8", json);
}
void HttpServerChat::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 HttpServerChat::generateHTML()
{
// Генерация CSS для шрифтов
QString fontFaces;
for (const QString &fontFile : m_fontList) {
QString fontName = fontFile;
fontName.replace(".ttf", "").replace(".otf", "").replace(".ttc", "");
fontFaces += QString("@font-face { font-family: '%1'; src: url(fonts/%2); }\n")
.arg(fontName).arg(fontFile);
}
QString deleteByTimeJS = m_deleteByTime ? "true" : "false";
QString html =
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
"<meta charset='UTF-8'>\n"
"<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\">\n"
"<meta http-equiv=\"Pragma\" content=\"no-cache\">\n"
"<meta http-equiv=\"Expires\" content=\"0\">\n"
"<title>Chat Messages</title>\n"
"<style>\n"
"body { background: %1; margin: 0; padding: 0; overflow: hidden; }\n"
"#messages { \n"
" position: fixed; \n"
" bottom: 20px; \n"
" right: 20px; \n"
" width: 400px; \n"
" display: flex; \n"
" flex-direction: column; \n"
" align-items: flex-end; \n"
"}\n"
".message { \n"
" margin: 3px 0; \n"
" border-radius: 5px; \n"
" transition: opacity 0.5s linear; \n"
" display: flex; \n"
" align-items: center; \n"
" opacity: 1;\n"
" max-width: 100%;\n"
" word-wrap: break-word;\n"
"}\n"
".message-icon { \n"
" width: 1.5em; \n"
" height: 1.5em; \n"
" margin-right: 0.5em; \n"
"}\n"
"%2\n" // font faces
"</style>\n"
"<script>\n"
"let existingMessages = new Map();\n"
"let fetching = false;\n"
"const deleteByTime = %3;\n"
"\n"
"function hexToRgb(hex) {\n"
" console.log('hexToRgb input:', hex);\n"
" \n"
" // Создаем временный canvas элемент для парсинга цвета\n"
" const ctx = document.createElement('canvas').getContext('2d');\n"
" ctx.fillStyle = hex;\n"
" const color = ctx.fillStyle;\n"
" \n"
" // Если цвет не распознан, используем значение по умолчанию\n"
" if (!color || color === 'rgba(0, 0, 0, 0)') {\n"
" console.log('Color not recognized, using default');\n"
" return '74, 175, 80'; // #4CAF50\n"
" }\n"
" \n"
" // Парсим цвет в формате rgb(r, g, b)\n"
" const match = color.match(/rgb\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)/);\n"
" \n"
" if (match) {\n"
" console.log('Parsed RGB:', match[1], match[2], match[3]);\n"
" return `${match[1]}, ${match[2]}, ${match[3]}`;\n"
" }\n"
" \n"
" console.log('Failed to parse, using default');\n"
" return '74, 175, 80'; // #4CAF50\n"
"}\n"
"\n"
"function fetchMessages() {\n"
" if (fetching) return;\n"
" fetching = true;\n"
" \n"
" fetch('/messages')\n"
" .then(response => response.json())\n"
" .then(data => {\n"
" const container = document.getElementById('messages');\n"
" \n"
" // 1. СОЗДАЕМ СЕТ ИЗ ВСЕХ ИМЕЮЩИХСЯ ID\n"
" const newIds = new Set();\n"
" data.forEach(msg => {\n"
" const msgId = 'msg-' + msg.timestamp;\n"
" newIds.add(msgId);\n"
" });\n"
" \n"
" // 2. УДАЛЯЕМ ТОЛЬКО ТЕ СООБЩЕНИЯ, КОТОРЫХ НЕТ В НОВЫХ ДАННЫХ\n"
" // И КОТОРЫЕ НЕ ЗАМОРОЖЕНЫ\n"
" existingMessages.forEach((div, msgId) => {\n"
" if (!newIds.has(msgId)) {\n"
" // Проверяем, заморожено ли сообщение\n"
" const isFreez = div.getAttribute('data-freez') === 'true';\n"
" if (!isFreez) {\n"
" div.style.opacity = '0';\n"
" setTimeout(() => {\n"
" if (div.parentNode) {\n"
" div.parentNode.removeChild(div);\n"
" }\n"
" existingMessages.delete(msgId);\n"
" }, 500);\n"
" }\n"
" }\n"
" });\n"
" \n"
" // 3. ДОБАВЛЯЕМ НОВЫЕ СООБЩЕНИЯ\n"
" data.forEach(msg => {\n"
" const msgId = 'msg-' + msg.timestamp;\n"
" \n"
" if (!existingMessages.has(msgId)) {\n"
" const div = document.createElement('div');\n"
" div.className = 'message';\n"
" div.id = msgId;\n"
" div.setAttribute('data-freez', msg.freez ? 'true' : 'false');\n"
" \n"
" // ПРИМЕНЯЕМ СТИЛИ\n"
" function hexToRgb(hex) {\n"
" console.log('hexToRgb input:', hex);\n"
" if (!hex || !hex.startsWith('#')) {\n"
" console.log('Not a hex color, using default');\n"
" return '74, 175, 80'; // #4CAF50\n"
" }\n"
" // Убираем # и проверяем длину\n"
" let cleanHex = hex.substring(1);\n"
" // Убираем альфа-канал если есть\n"
" if (cleanHex.length === 8) {\n"
" cleanHex = cleanHex.substring(0, 6);\n"
" }\n"
" // Расширяем короткую форму (#rgb → #rrggbb)\n"
" if (cleanHex.length === 3) {\n"
" cleanHex = cleanHex[0] + cleanHex[0] + \n"
" cleanHex[1] + cleanHex[1] + \n"
" cleanHex[2] + cleanHex[2];\n"
" }\n"
" // Проверяем, что строка состоит из шестнадцатеричных символов\n"
" if (cleanHex.length !== 6 || !/^[0-9A-Fa-f]{6}$/.test(cleanHex)) {\n"
" console.log('Invalid hex format, using default');\n"
" return '74, 175, 80'; // #4CAF50\n"
" }\n"
" // Конвертируем HEX в RGB\n"
" const r = parseInt(cleanHex.substring(0, 2), 16);\n"
" const g = parseInt(cleanHex.substring(2, 4), 16);\n"
" const b = parseInt(cleanHex.substring(4, 6), 16);\n"
" console.log('Parsed RGB:', r, g, b);\n"
" return `${r}, ${g}, ${b}`;\n"
" }\n"
" \n"
" // ФОРМИРУЕМ СОДЕРЖИМОЕ\n"
" let content = '<span><b>' + msg.nickname + ':</b> ' + msg.content + '</span>';\n"
" div.innerHTML = content;\n"
" div.style.opacity = '1';\n"
" \n"
" container.appendChild(div);\n"
" existingMessages.set(msgId, div);\n"
" \n"
" // 4. УСТАНАВЛИВАЕМ ТАЙМЕР УДАЛЕНИЯ ТОЛЬКО ЕСЛИ:\n"
" // - включено удаление по времени (deleteByTime)\n"
" // - сообщение НЕ заморожено (freez = false)\n"
" if (deleteByTime && (!msg.freez || msg.freez === false)) {\n"
" setTimeout(() => {\n"
" if (existingMessages.has(msgId)) {\n"
" const messageDiv = existingMessages.get(msgId);\n"
" messageDiv.style.opacity = '0';\n"
" setTimeout(() => {\n"
" if (messageDiv.parentNode) {\n"
" messageDiv.parentNode.removeChild(messageDiv);\n"
" }\n"
" existingMessages.delete(msgId);\n"
" }, 500);\n"
" }\n"
" }, (parseInt(msg.timeMsg) || 10) * 1000);\n"
" }\n"
" }\n"
" });\n"
" \n"
" // 5. ОБРАБОТКА РЕЖИМА УДАЛЕНИЯ ПО КОЛИЧЕСТВУ\n"
" if (!deleteByTime) {\n"
" const maxMessages = 100;\n"
" if (existingMessages.size > maxMessages) {\n"
" const messagesArray = Array.from(existingMessages.entries());\n"
" messagesArray.sort((a, b) => {\n"
" const idA = parseInt(a[0].replace('msg-', ''));\n"
" const idB = parseInt(b[0].replace('msg-', ''));\n"
" return idA - idB;\n"
" });\n"
" \n"
" let removedCount = 0;\n"
" for (const [msgId, div] of messagesArray) {\n"
" if (existingMessages.size - removedCount <= maxMessages) break;\n"
" \n"
" const isFreez = div.getAttribute('data-freez') === 'true';\n"
" if (!isFreez) {\n"
" div.style.opacity = '0';\n"
" setTimeout(() => {\n"
" if (div.parentNode) {\n"
" div.parentNode.removeChild(div);\n"
" }\n"
" existingMessages.delete(msgId);\n"
" }, 500);\n"
" removedCount++;\n"
" }\n"
" }\n"
" }\n"
" }\n"
" })\n"
" .catch(error => console.error('Ошибка при загрузке сообщений:', error))\n"
" .finally(() => { fetching = false; });\n"
"}\n"
"\n"
"// Обновляем сообщения каждые 500 мс\n"
"setInterval(fetchMessages, 500);\n"
"\n"
"// Первоначальная загрузка\n"
"fetchMessages();\n"
"</script>\n"
"</head>\n"
"<body>\n"
"<div id='messages'></div>\n"
"</body>\n"
"</html>";
return html.arg(m_backgroundColor).arg(fontFaces).arg(deleteByTimeJS);
}
QString HttpServerChat::generateJSON()
{
QJsonArray jsonArray;
for (const ChatMessage &msg : m_messages) {
QJsonObject obj;
obj["nickname"] = msg.nickname;
obj["content"] = msg.content;
obj["timestamp"] = msg.timestamp;
obj["timeMsg"] = msg.timeMsg;
obj["freez"] = msg.freez;
obj["transparency"] = msg.transparency;
// Конвертируем цветовые названия в HEX формат
QString blockColor = msg.blockColor.isEmpty() ? m_currentStyle.blockColor : msg.blockColor;
QString borderColor = msg.borderColor.isEmpty() ? m_currentStyle.borderColor : msg.borderColor;
QString fontColor = msg.fontColor.isEmpty() ? m_currentStyle.fontColor : msg.fontColor;
// Конвертируем названия цветов в HEX формат
obj["color"] = colorNameToHex(blockColor);
obj["borderColor"] = colorNameToHex(borderColor);
obj["fontColor"] = colorNameToHex(fontColor);
obj["family"] = msg.fontFamily.isEmpty() ? m_currentStyle.fontFamily : msg.fontFamily;
obj["fontSize"] = msg.fontSize > 0 ? msg.fontSize : m_currentStyle.fontSize;
obj["borderSize"] = msg.borderSize > 0 ? msg.borderSize : m_currentStyle.borderSize;
obj["padding"] = msg.padding > 0 ? msg.padding : m_currentStyle.padding;
jsonArray.append(obj);
}
QJsonDocument doc(jsonArray);
return QString::fromUtf8(doc.toJson());
}
QString HttpServerChat::colorNameToHex(const QString &colorName) const
{
if (colorName.isEmpty()) {
return "#000000";
}
// Если строка уже начинается с #, это HEX формат
if (colorName.startsWith('#')) {
return colorName;
}
// Проверяем, является ли это названием цвета
QColor color;
if (color.isValidColor(colorName)) {
color.setNamedColor(colorName);
return color.name();
}
// Пробуем создать QColor из строки
color = QColor(colorName);
if (color.isValid()) {
return color.name();
}
// Если ничего не получилось, возвращаем черный по умолчанию
return "#000000";
}
void HttpServerChat::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);
socket->flush();
}
QString HttpServerChat::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";
return "text/plain";
}
void HttpServerChat::discardClient()
{
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
if (socket) {
m_clients.removeOne(socket);
socket->deleteLater();
}
}
// Реализация методов получения настроек
QString HttpServerChat::getFontFamily() const {
return m_currentStyle.fontFamily;
}
int HttpServerChat::getFontSize() const {
return m_currentStyle.fontSize;
}
QString HttpServerChat::getFontColor() const {
return m_currentStyle.fontColor;
}
QString HttpServerChat::getBlockColor() const {
return m_currentStyle.blockColor;
}
QString HttpServerChat::getBorderColor() const {
return m_currentStyle.borderColor;
}
int HttpServerChat::getBorderSize() const {
return m_currentStyle.borderSize;
}
int HttpServerChat::getPadding() const {
return m_currentStyle.padding;
}
int HttpServerChat::getMessageTimeout() const {
return m_currentStyle.timeMsg;
}
QString HttpServerChat::getBackgroundColor() const {
return m_backgroundColor;
}
int HttpServerChat::getTransparency() const {
return m_currentStyle.transparency;
}
void HttpServerChat::setFontFamily(const QString &fontFamily) {
m_currentStyle.fontFamily = fontFamily;
}
void HttpServerChat::setFontSize(int fontSize) {
m_currentStyle.fontSize = fontSize;
}
void HttpServerChat::setFontColor(const QString &fontColor) {
m_currentStyle.fontColor = fontColor;
}
void HttpServerChat::setBlockColor(const QString &blockColor) {
m_currentStyle.blockColor = blockColor;
}
void HttpServerChat::setBorderColor(const QString &borderColor) {
m_currentStyle.borderColor = borderColor;
}
void HttpServerChat::setBorderSize(int borderSize) {
m_currentStyle.borderSize = borderSize;
}
void HttpServerChat::setPadding(int padding) {
m_currentStyle.padding = padding;
}
void HttpServerChat::setMessageTimeout(int timeout) {
m_currentStyle.timeMsg = timeout;
}
void HttpServerChat::setTransparency(int transparency) {
m_currentStyle.transparency = qBound(0, transparency, 255);
}
void HttpServerChat::setDeleteMode(bool deleteByTime, int maxMsgCount)
{
m_deleteByTime = deleteByTime;
m_maxMsgCount = maxMsgCount;
if (!deleteByTime) {
// Режим удаления по количеству
// Удаляем самые старые сообщения, НО сохраняем замороженные
while (m_messages.size() > m_maxMsgCount) {
// Ищем незамороженное сообщение для удаления
bool removed = false;
for (int i = 0; i < m_messages.size(); ++i) {
if (!m_messages[i].freez) {
m_messages.removeAt(i);
removed = true;
break;
}
}
// Если все сообщения заморожены, выходим из цикла
if (!removed) {
break;
}
}
} else {
// Режим удаления по времени
// Замороженные сообщения сохраняются в любом случае
cleanupOldMessages();
}
}
void HttpServerChat::cleanupOldMessages()
{
if (!m_deleteByTime) {
return; // Если удаление по времени отключено, выходим
}
qint64 currentTime = QDateTime::currentSecsSinceEpoch();
// Проходим сообщения в обратном порядке для безопасного удаления
for (int i = m_messages.size() - 1; i >= 0; --i) {
const ChatMessage &msg = m_messages[i];
// Пропускаем замороженные сообщения
if (msg.freez) {
continue;
}
qint64 messageAge = currentTime - msg.timestamp;
if (messageAge >= msg.timeMsg) {
m_messages.removeAt(i);
}
}
}