ircabot/httpserver.cpp

2097 lines
82 KiB
C++

#include "httpserver.h"
#include "ircclient.h"
#include "connectiondata.h"
#include "global.h"
#include "version.h"
#include <QRegularExpression>
#include <QDirIterator>
#include <QDateTime>
#include <QHostAddress>
#include <QTcpSocket>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QFile>
#include <QDir>
const char CRITICAL_ERROR[] {"<title>Critical error</title><center><h1>NOT FOUND</H1><p>Maybe it's compile time error</p></center>"};
HttpServer::HttpServer(const QString &address, quint16 port, const QString& dataFolder,
const QString& serviceName, const QString& serviceEmoji, bool ajaxIsDisabled, QObject *parent) :
QObject(parent),
m_TcpServer(new QTcpServer),
m_serviceName(serviceName),
m_dataFolder(dataFolder),
m_ajaxIsDisabled(ajaxIsDisabled)
{
if (not m_TcpServer->listen(QHostAddress(address), port)) {
throw std::runtime_error("HttpServer not binded at " +
address.toStdString() + " : " + QString::number(port).toStdString());
}
consoleLog(address + " : " + QString::number(port));
if (m_ajaxIsDisabled) {
consoleLog("JavaScript on webpages removed and AJAX disabled!");
}
if (not QFile::exists(m_dataFolder+"main_page.txt")) {
QFile mp(m_dataFolder+"main_page.txt");
if (mp.open(QIODevice::WriteOnly)) {
mp.write("# Main page file.\n"
"# HTML is supported. For line breaks, use <br> or <p></p>.\n\n"
"<center>\n"
"# Your images from \"" + m_dataFolder.toUtf8() +
"custom_images\" must have URL \"/~images/\"\n"
" <img src=\"/~images/example.png\">\n"
"</center>\n");
mp.close();
}
else {
throw std::runtime_error("main_page.txt not exist and creating failed");
}
}
QDir dir {m_dataFolder};
if (not dir.cd("custom_images")) {
if (dir.mkdir("custom_images")) {
dir.cd("custom_images");
QFile examplePng(dir.path()+global::slash+"example.png");
if (examplePng.open(QIODevice::WriteOnly)) {
QFile nativePng("://html/custom_img_example.png");
if (nativePng.open(QIODevice::ReadOnly)) {
examplePng.write(nativePng.readAll());
nativePng.close();
examplePng.close();
}
}
} else {
consoleLog("Creating folder \"custom_images\" failed");
}
}
m_serviceButton = HTML_LEFT_MENU_MAIN_POINT;
replaceTag(m_serviceButton, "EMOJI", serviceEmoji);
replaceTag(m_serviceButton, "SERVICE_NAME", serviceName);
connect (m_TcpServer, &QTcpServer::newConnection, this, &HttpServer::acceptor);
}
HttpServer::~HttpServer()
{
m_TcpServer->close();
m_TcpServer->deleteLater();
}
QString HttpServer::convertToClickableLink(const QString &httpLine)
{
QString result;
if (not httpLine.contains(QRegularExpression("http.?://"))) return result;
QString displayedName {httpLine};
displayedName.remove(QRegularExpression("http.?://(www\\.)?"));
displayedName.remove(QRegularExpression("/$"));
result = "<a href=\"" + httpLine + "\"> " + displayedName + " </a>";
return result;
}
std::pair<QString, QString> HttpServer::splitUserNameAndMessage(const QString &rawLine)
{
std::pair<QString, QString> result;
QString nick {rawLine};
nick.remove(QRegularExpression("\\]\\s.*$"));
nick.remove(QRegularExpression("^\\["));
if (nick.isEmpty()) {
return result;
}
nick = nick.toHtmlEscaped();
// long nicks
if (nick.size() > MAX_NICKNAME_LENGTH_WITHOUT_WBR) {
int lastWbr = 0;
for (int i = 0; i < nick.size(); i++) {
if (i-lastWbr > MAX_NICKNAME_LENGTH_WITHOUT_WBR) {
nick.insert(i, "<wbr>");
lastWbr = i;
}
}
}
QString text {rawLine};
text.remove(QRegularExpression("^\\[[^\\s]*\\]\\s"));
if (text.isEmpty()) {
return result;
}
text = text.toHtmlEscaped();
// http links
bool linksFound {false};
while (QRegularExpression("(^|\\s)http.?://").match(text).hasMatch()) {
if (not linksFound) linksFound = true;
int pos = text.indexOf(QRegularExpression("(^|\\s)http.?://"));
if (pos == -1) {
consoleLog("Bug! HttpServer.cpp while (QRegularExpression(\"(^|\\s)http.?://\").match(text).hasMatch())");
break;
}
QString rawLink {text};
rawLink.remove(0, pos);
if (rawLink.startsWith(' ')) {
rawLink.remove(0,1);
}
int space = rawLink.indexOf(' ');
if (space > 0) {
rawLink.remove(space, rawLink.size()-space);
}
text.replace(rawLink, convertToClickableLink(rawLink));
}
// long lines
int space = 0;
bool nbTag = false; // For safe HTML tags like a &it; via <wbr>!
bool isHref = false;
for (int i = 0; i < text.size(); i++) {
if (text[i] == ' ') {
space = i;
if (isHref) {
isHref = false;
}
else {
if (text.indexOf("href=\"http", i+1) == i+1) {
isHref = true;
}
}
}
if (nbTag and text[i-1] == ';') {
nbTag = false;
}
if (text.indexOf(QRegularExpression("(\\&amp;|\\&lt;|\\&gt;|\\&quot;).*"), i) == i) {
nbTag = true;
}
if (not isHref and i-space > MAX_MESSAGE_LENGTH_WITHOUT_WBR and not nbTag) {
text.insert(i, "<wbr>");
space = i;
}
}
if (linksFound) text.replace(" </a>", "</a>"); // delete whitespace in links end
result.first = nick;
result.second = text;
return result;
}
void HttpServer::consoleLog(const QString &message)
{
qInfo().noquote() << "[WEB_UI]" << message;
}
void HttpServer::debugLog(const QString &req)
{
QFile log(m_dataFolder + "debug.log");
if (log.open(QIODevice::WriteOnly | QIODevice::Append)) {
log.write(QDateTime::currentDateTime().toString().toUtf8() + ":\n" + req.toUtf8() + "\n\n");
log.close();
}
}
void HttpServer::acceptor()
{
QTcpSocket* socket = m_TcpServer->nextPendingConnection();
connect(socket, &QTcpSocket::readyRead, this, &HttpServer::reader);
connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater);
}
void HttpServer::reader()
{
QTcpSocket* socket = static_cast<QTcpSocket*>(sender());
QString request = socket->read(BUFFER_SIZE);
if (not request.startsWith("GET") and not request.startsWith("HEAD")) {
if (socket->isOpen()) {
socket->write("Your request has been rejected!\n");
socket->disconnectFromHost();
}
return;
}
bool isHeadRequest = false;
if (request.startsWith("HEAD ")) {
isHeadRequest = true;
}
QString urlPath = getRequestPath(request);
if (urlPath == "/favicon.ico") {
QString eTag = global::getValue(request, "If-None-Match", global::eHttpHeader);
if (eTag == HTTP_ACTUAL_ETAG) {
if (socket->isOpen()) socket->write(HEADER_304.toUtf8());
}
else {
QFile icon("://html/favicon.ico");
if (icon.open(QIODevice::ReadOnly)) {
QByteArray file = icon.readAll();
icon.close();
QString header = HEADER_ICO;
replaceTag(header, "SIZE", QString::number(file.size()));
if (socket->isOpen()) {
socket->write(header.toUtf8());
if (not isHeadRequest) socket->write(file);
}
}
}
}
else if (urlPath == "/") {
writeMainPage(socket, isHeadRequest);
}
else if (urlPath == "/newmessage.mp3" and not m_ajaxIsDisabled) {
QString eTag = global::getValue(request, "If-None-Match", global::eHttpHeader);
if (eTag == HTTP_ACTUAL_ETAG) {
if (socket->isOpen()) socket->write(HEADER_304.toUtf8());
}
else {
QFile mp3("://html/newmessage.mp3");
if (mp3.open(QIODevice::ReadOnly)) {
QByteArray file = mp3.readAll();
mp3.close();
QString header = HEADER_MP3;
replaceTag(header, "SIZE", QString::number(file.size()));
if (socket->isOpen()) {
socket->write(header.toUtf8());
if (not isHeadRequest) socket->write(file);
}
}
}
}
else if (urlPath == "/style.css") {
QString eTag = global::getValue(request, "If-None-Match", global::eHttpHeader);
if (eTag == HTTP_ACTUAL_ETAG) {
if (socket->isOpen()) socket->write(HEADER_304.toUtf8());
}
else {
QFile css("://html/style.css");
if (css.open(QIODevice::ReadOnly)) {
QByteArray file = css.readAll();
css.close();
QString header = HEADER_CSS;
replaceTag(header, "SIZE", QString::number(file.size()));
if (socket->isOpen()) {
socket->write(header.toUtf8());
if (not isHeadRequest) socket->write(file);
}
}
}
}
else if (urlPath.endsWith(".svg")) {
QString eTag = global::getValue(request, "If-None-Match", global::eHttpHeader);
if (eTag == HTTP_ACTUAL_ETAG) {
if (socket->isOpen()) socket->write(HEADER_304.toUtf8());
}
else {
QFile svg("://html"+urlPath);
if (svg.open(QIODevice::ReadOnly)) {
QByteArray file = svg.readAll();
svg.close();
QString header = HEADER_SVG;
replaceTag(header, "SIZE", QString::number(file.size()));
if (socket->isOpen()) {
socket->write(header.toUtf8());
if (not isHeadRequest) socket->write(file);
}
}
else {
if (socket->isOpen()) {
socket->write(HEADER_404.toUtf8());
if (not isHeadRequest) writeErrorPage(socket);
}
}
}
}
else if (urlPath == "/realtimechat.js" and not m_ajaxIsDisabled) {
QFile js("://html/realtimechat.js");
if (js.open(QIODevice::ReadOnly)) {
QByteArray file = js.readAll();
js.close();
QString header = HEADER_JS;
replaceTag(header, "SIZE", QString::number(file.size()));
if (socket->isOpen()) {
socket->write(header.toUtf8());
if (not isHeadRequest) socket->write(file);
}
}
}
else if (urlPath.startsWith("/ajax/")) {
if (m_ajaxIsDisabled) {
writeErrorJson(socket, "AJAX disabled");
} else {
writeAjaxAnswer(socket, urlPath, isHeadRequest);
}
}
else if (urlPath.startsWith("/realtimereadingchat/")) {
if (m_ajaxIsDisabled) {
writeErrorPage(socket, "DISABLED"
"<p>No JS, no AJAX, no real time reading!</p>");
} else {
writeRealTimeChatPage(socket, urlPath, isHeadRequest);
}
}
else if (urlPath.startsWith("/~images/")) {
writeCustomPicture(socket, urlPath, isHeadRequest);
}
else {
writeRegularPage(socket, urlPath, isHeadRequest);
}
socket->disconnectFromHost();
}
void HttpServer::ircBotFirstInfo(QString server, QStringList channels)
{
for (const auto &c: channels) {
if (c.isEmpty()) continue;
m_servers[server][c] = QStringList();
}
}
void HttpServer::ircUsersOnline(QString server, QString channel, QStringList users)
{
if (server.isEmpty()) return;
if (channel.isEmpty()) return;
QStringList sortedNicknames;
QStringList ownersNicks;
QStringList operNicks;
QStringList halfopNicks;
QStringList adminNicks;
QStringList voicedNicks;
QStringList plainNicks;
for (auto rawOneNick: users) {
rawOneNick = rawOneNick.toHtmlEscaped();
if (rawOneNick.startsWith('~')) {
ownersNicks.push_back(rawOneNick);
}
else if (rawOneNick.startsWith('@')) {
operNicks.push_back(rawOneNick);
}
else if (rawOneNick.startsWith('%')) {
halfopNicks.push_back(rawOneNick);
}
else if (rawOneNick.startsWith('&')) {
adminNicks.push_back(rawOneNick);
}
else if (rawOneNick.startsWith('+')) {
voicedNicks.push_back(rawOneNick);
}
else {
plainNicks.push_back(rawOneNick);
}
}
if (not ownersNicks.isEmpty()) {
std::sort(ownersNicks.begin(), ownersNicks.end());
sortedNicknames += ownersNicks;
}
if (not operNicks.isEmpty()) {
std::sort(operNicks.begin(), operNicks.end());
sortedNicknames += operNicks;
}
if (not halfopNicks.isEmpty()) {
std::sort(halfopNicks.begin(), halfopNicks.end());
sortedNicknames += halfopNicks;
}
if (not adminNicks.isEmpty()) {
std::sort(adminNicks.begin(), adminNicks.end());
sortedNicknames += adminNicks;
}
if (not voicedNicks.isEmpty()) {
std::sort(voicedNicks.begin(), voicedNicks.end());
sortedNicknames += voicedNicks;
}
if (not plainNicks.isEmpty()) {
std::sort(plainNicks.begin(), plainNicks.end());
sortedNicknames += plainNicks;
}
sortedNicknames.removeAll("");
m_servers[server][channel] = sortedNicknames;
}
void HttpServer::ircChannelTopic(QString server, QString channel, QString topic)
{
m_channelsTopic[server][channel] = topic;
}
void HttpServer::ircServerOnline(QString server, quint8 status)
{
if (server.isEmpty()) return;
bool online = status;
m_serversOnline[server] = online;
}
void HttpServer::ircBotNick(QString server, QString nickname)
{
m_botNick[server] = nickname;
}
void HttpServer::ircMessageCache(QString server, QString channel, QString nick, QString text)
{
QString channelId {server+channel};
if (not m_messageCache.contains(channelId)) return;
// remove timed out session
if (QDateTime::currentMSecsSinceEpoch() - MSECS_TO_AUTOREMOVE_MESSAGES_FROM_BUFFER >
m_messageCache[channelId]->getLastPing())
{
consoleLog("Message caching disabled for "+server+"/#"+channel+". No active reader.");
m_messageCache.remove(channelId);
return;
}
else {
auto processedMsg {splitUserNameAndMessage("["+nick+"] " + text)};
m_messageCache[channelId]->saveNewMessage(processedMsg.first, processedMsg.second);
}
}
QString HttpServer::getRequestPath(const QString &req)
{
if (req.isEmpty()) return QString();
QString result(req);
int begin = result.indexOf(' ');
if (begin == -1) return QString();
result.remove(0, begin+1);
int space = result.indexOf(' ');
int size = result.size();
result.remove(space, size-space);
result = QByteArray::fromPercentEncoding(result.toUtf8());
return result;
}
QString HttpServer::getWordFromPath(const QString &path)
{
QString result {path};
result.remove(QRegularExpression("\\?.*$")); // any actions like a ?toSearch=
if (result.startsWith('/')) {
result.remove(QRegularExpression("^/"));
}
result.remove(QRegularExpression("/.*$"));
return result;
}
void HttpServer::writeMainPage(QTcpSocket *socket, bool isHeadRequest)
{
auto renderStart = QDateTime::currentMSecsSinceEpoch();
QFile main("://html/main.html");
QString page;
if (main.open(QIODevice::ReadOnly)) {
page = main.readAll();
main.close();
}
else {
if (socket->isOpen()) {
socket->write(HEADER_404.toUtf8());
if (not isHeadRequest) socket->write(CRITICAL_ERROR);
}
}
replaceTag(page, "PAGE_TITLE", m_serviceName + " | IRC logger");
//// Left menu compilation
QString htmlServersSectionS;
for (const auto &s: m_servers) {
if (s.first.isEmpty()) continue; // empty server name?
QString htmlServersSection = HTML_SERVER_SECTION;
replaceTag(htmlServersSection, "ABOUT_SERVER", "/"+s.first);
replaceTag(htmlServersSection, "SERVER_NAME", s.first);
QString htmlChannelLineS;
for (const auto &c: s.second) {
QString htmlChannelLine;
htmlChannelLine = HTML_SERVER_SECTION_CHANNEL;
replaceTag(htmlChannelLine, "CHANNEL_NAME", c.first);
QString channelNameForUrl {c.first};
channelNameForUrl.remove('#');
QString channelLink = "/" + global::toLowerAndNoSpaces(s.first) + "/" + channelNameForUrl;
replaceTag(htmlChannelLine, "CHANNEL_LINK", channelLink);
htmlChannelLineS += htmlChannelLine;
}
replaceTag(htmlServersSection, "CHANNELS", htmlChannelLineS);
bool online {false};
for (const auto &srv: m_serversOnline) {
if (srv.first == s.first) {
online = srv.second;
break;
}
}
if (online) {
replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_ONLINE_MARKER);
} else {
replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_OFFLINE_MARKER);
}
htmlServersSectionS += htmlServersSection;
}
QString serviceButtonSelected {m_serviceButton};
serviceButtonSelected.replace("class=\"left_menu__mainitem\"",
"class=\"left_menu__mainitem\" style=\"opacity: 1\"");
htmlServersSectionS.push_front(serviceButtonSelected);
replaceTag(page, "SERVERS_SECTION", htmlServersSectionS);
page.remove(QRegularExpression("<div class=\"main_header\">.*<!-- main_middle -->", QRegularExpression::DotMatchesEverythingOption));
QString payloadBlock = HTML_PAYLOAD_ABOUT;
payloadBlock.remove("<span style=\"color: green; display: block; font-size: 24px; text-align: center;\">{{ABOUT_TITLE}}</span><br>\n");
QString aboutBlock;
QFile mp(m_dataFolder+global::slash+"main_page.txt");
if (mp.open(QIODevice::ReadOnly)) {
QString rbuffer = mp.readLine();
while (not rbuffer.isEmpty()) {
if (rbuffer.startsWith('#')) {
rbuffer = mp.readLine();
continue;
}
removeBrakelineSymbols(rbuffer);
if (not rbuffer.isEmpty()) {
aboutBlock += rbuffer;
}
rbuffer = mp.readLine();
}
}
else {
aboutBlock = "No information provided";
}
replaceTag(payloadBlock, "ABOUT_TEXT", aboutBlock);
replaceTag(page, "PAYLOAD_BLOCK", payloadBlock);
//// Footer
replaceTag(page, "VERSION", IRCABOT_VERSION);
replaceTag(page, "COPYRIGHT_YEAR", COPYRIGHT_YEAR);
//// Finish
replaceTag(page, "RENDERING_TIMER", QString::number(QDateTime::currentMSecsSinceEpoch() - renderStart));
QString mainHeader = HEADER_HTML;
replaceTag(mainHeader, "SIZE", QString::number(QByteArray(page.toUtf8()).size()));
if (socket->isOpen()) {
socket->write(mainHeader.toUtf8());
if (not isHeadRequest) socket->write(page.toUtf8());
}
}
void HttpServer::writeCustomPicture(QTcpSocket *socket, QString &urlPath, bool isHeadRequest)
{
QString fileName {urlPath};
fileName.remove(QRegularExpression("^/~images/"));
QFile f {m_dataFolder+global::slash+"custom_images"+global::slash+fileName};
if (not f.exists()) {
writeErrorPage(socket, fileName + "<p>IMAGE NOT FOUND</p>");
return;
}
QByteArray file;
if (f.open(QIODevice::ReadOnly)) {
file = f.readAll();
f.close();
}
else {
writeErrorPage(socket, "FORBIDDEN");
return;
}
QString picHeader = HEADER_IMG;
QString fileType {fileName};
fileType.remove(QRegularExpression(".*\\."));
replaceTag(picHeader, "TYPE", fileType);
replaceTag(picHeader, "SIZE", QString::number(file.size()));
if (socket->isOpen()) {
socket->write(picHeader.toUtf8());
if (not isHeadRequest) socket->write(file);
}
}
void HttpServer::writeErrorPage(QTcpSocket *socket, const QString& text)
{
if (socket->isOpen()) {
socket->write(HEADER_404.toUtf8());
socket->write("<title>404</title><center><h1>"+text.toUtf8()+"</H1></center>");
}
}
void HttpServer::writeErrorJson(QTcpSocket * socket, const QString &text)
{
QString header = HEADER_JSON;
QByteArray body {
"{\"status\": false, \"message\": \"" + text.toUtf8() + "\"}"
};
replaceTag(header, "SIZE", QString::number(body.size()));
socket->write(header.toUtf8());
socket->write(body);
}
void HttpServer::removeBrakelineSymbols(QString &line)
{
line.remove('\r');
line.remove('\n');
line.remove('\t');
}
inline void HttpServer::replaceTag(QString &page, const QString &tag, const QString &payload)
{
page.replace("{{"+tag+"}}", payload);
}
void HttpServer::writeRegularPage(QTcpSocket *socket, QString &urlPath, bool isHeadRequest)
{
auto renderStart = QDateTime::currentMSecsSinceEpoch();
QString searchRequest;
bool isRegexp {false};
int specSymbol = urlPath.indexOf('?'); // any actions like a ?toSearch=
if (specSymbol != -1) {
searchRequest = global::getValue(urlPath, "toSearch", global::eForWeb);
isRegexp = global::getValue(urlPath, "isRegexp", global::eForWeb) == "on";
urlPath.remove(specSymbol, urlPath.size()-specSymbol);
}
QString server = getWordFromPath(urlPath);
QDir fsPath(m_dataFolder+server);
if (not fsPath.exists()) {
if (isHeadRequest) {
if (socket->isOpen()) socket->write(HEADER_404.toUtf8());
} else {
writeErrorPage(socket, "REQUESTED SERVER LOG NOT EXIST");
}
return;
}
urlPath.remove(QRegularExpression("^.*/"+server));
QString channel = getWordFromPath(urlPath);
channel.remove(QRegularExpression("\\?.*$"));
if (channel.isEmpty()) {
writeAboutServerPage(socket, server, isHeadRequest);
return;
}
if (not fsPath.cd(channel)) {
if (isHeadRequest) {
if (socket->isOpen()) socket->write(HEADER_404.toUtf8());
} else {
writeErrorPage(socket, "REQUESTED CHANNEL LOG NOT EXIST");
}
return;
}
QString originalServerName;
for (const auto &s: m_servers) {
if (global::toLowerAndNoSpaces(s.first) == server) {
originalServerName = s.first;
}
}
QString originalChannelName;
for (const auto &server: m_servers) {
for (const auto &channel_users: server.second) {
if (global::toLowerAndNoSpaces(channel_users.first) == "#"+channel) {
originalChannelName = global::toLowerAndNoSpaces(channel_users.first);
}
}
}
urlPath.remove(QRegularExpression("^.*/"+channel));
QString year = getWordFromPath(urlPath);
year.remove(QRegularExpression("\\?.*$"));
QString month;
QString day;
if (not year.isEmpty() and fsPath.cd(year)) {
urlPath.remove(QRegularExpression("^.*/"+year));
month = getWordFromPath(urlPath);
month.remove(QRegularExpression("\\?.*$"));
if (not month.isEmpty() and fsPath.cd(month)) {
if (urlPath.startsWith("/"+month+"/")) {
urlPath.remove(0,1);
int pos = urlPath.indexOf('/');
if (pos != -1) {
urlPath.remove(0,pos);
day = getWordFromPath(urlPath);
if (urlPath.endsWith(".txt")) {
QFile plain(fsPath.path()+global::slash+day);
if (plain.open(QIODevice::ReadOnly)) {
QString header = HEADER_TEXT;
QByteArray file = plain.readAll();
plain.close();
replaceTag(header, "SIZE", QString::number(file.size()));
if (socket->isOpen()) {
socket->write(header.toUtf8());
if (not isHeadRequest) socket->write(file);
}
}
else {
if (isHeadRequest) {
if (socket->isOpen()) socket->write(HEADER_404.toUtf8());
} else {
writeErrorPage(socket, "FILE OPENING FAILED");
}
}
return;
}
else {
if (not QFile::exists(fsPath.path()+global::slash+day+".txt")) {
day.clear();
}
}
}
}
}
else { month.clear(); }
}
else { year.clear(); }
QFile main("://html/main.html");
QString page;
if (main.open(QIODevice::ReadOnly)) {
page = main.readAll();
main.close();
}
else {
if (socket->isOpen()) {
socket->write(HEADER_404.toUtf8());
if (not isHeadRequest) socket->write(CRITICAL_ERROR);
}
}
if (isRegexp) {
page.replace("<input id=\"main_header__search_checkbox__button\" type=\"checkbox\" name=\"isRegexp\">",
"<input id=\"main_header__search_checkbox__button\" type=\"checkbox\" name=\"isRegexp\" checked>");
}
//// Left menu compilation
QString htmlServersSectionS;
for (const auto &s: m_servers) {
if (s.first.isEmpty()) continue; // empty server name?
QString htmlServersSection = HTML_SERVER_SECTION;
replaceTag(htmlServersSection, "ABOUT_SERVER", "/"+s.first);
replaceTag(htmlServersSection, "SERVER_NAME", s.first);
QString htmlChannelLineS;
for (const auto &c: s.second) {
QString htmlChannelLine;
if (originalServerName == s.first and originalChannelName == c.first) {
htmlChannelLine = HTML_SERVER_SECTION_CHANNEL_SELECTED;
} else {
htmlChannelLine = HTML_SERVER_SECTION_CHANNEL;
}
replaceTag(htmlChannelLine, "CHANNEL_NAME", c.first);
QString channelNameForUrl {c.first};
channelNameForUrl.remove('#');
QString channelLink = "/" + global::toLowerAndNoSpaces(s.first) + "/" + channelNameForUrl;
if (not year.isEmpty()) {
channelLink += "/" + year;
if (not month.isEmpty()) {
channelLink += "/" + month;
if (not day.isEmpty()) {
channelLink += "/" + day;
}
}
}
replaceTag(htmlChannelLine, "CHANNEL_LINK", channelLink);
htmlChannelLineS += htmlChannelLine;
}
replaceTag(htmlServersSection, "CHANNELS", htmlChannelLineS);
bool online {false};
for (const auto &srv: m_serversOnline) {
if (srv.first == s.first) {
online = srv.second;
break;
}
}
if (online) {
replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_ONLINE_MARKER);
} else {
replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_OFFLINE_MARKER);
}
if (s.first == originalServerName) {
htmlServersSectionS.push_front(htmlServersSection);
} else {
htmlServersSectionS += htmlServersSection;
}
}
htmlServersSectionS.push_front(m_serviceButton);
replaceTag(page, "SERVERS_SECTION", htmlServersSectionS);
//// Main section header compilation
QString& topic = m_channelsTopic[originalServerName][originalChannelName];
topic = topic.replace('\"', "&quot;");
QString titlePostfix = " | " + m_serviceName;
if (not topic.isEmpty()) {
titlePostfix.push_front(" | " + topic);
}
if (m_servers.size() > 1) {
replaceTag(page, "PAGE_TITLE", originalChannelName + " ("+originalServerName+")" + titlePostfix);
} else {
replaceTag(page, "PAGE_TITLE", originalChannelName + titlePostfix);
}
replaceTag(page, "CHANNEL_TOPIC", topic);
replaceTag(page, "MAIN_HEADER", originalChannelName);
if (m_ajaxIsDisabled) {
page.remove("<a href=\"{{REALTIME_LINK}}\" title=\"{{AIRPLAIN_TITLE}}\" class=\"main_header__title_airplaine\"></a>");
}
else {
replaceTag(page, "REALTIME_LINK", "/realtimereadingchat/"+server+"/"+channel);
replaceTag(page, "AIRPLAIN_TITLE", "Read in real time");
}
QString middlePath = "<a style=\"text-decoration: none\" href=\"/"+server+"/"+channel+"\">" + "/" + "</a>";
if (not year.isEmpty()) {
middlePath += "<a style=\"text-decoration: none\" href=\"/"+server+"/"+channel+"/"+year+"\">" + year + "</a>";
if (not month.isEmpty()) {
middlePath += "/<a style=\"text-decoration: none\" href=\"/"+server+"/"+channel+"/"+year+"/"+month+"\">" + month + "</a>";
}
}
QString arrows = HTML_PAYLOAD_ADDITIONAL_ARROWS;
QString currentYear {QDateTime::currentDateTime().toString("yyyy")};
QString currentMonth {QDateTime::currentDateTime().toString("MM")};
QString currentDay {QDateTime::currentDateTime().toString("dd")};
replaceTag(arrows, "CURRENT_DATA_LOG", "/"+server+"/"+channel+"/"+
currentYear+"/"+currentMonth+"/"+currentDay);
replaceTag(page, "ADDITIONAL_BUTTON", arrows);
int currentOnline = 0;
QString onlineUserS;
for (const auto &user: m_servers[originalServerName][originalChannelName]) {
if (QRegularExpression("^(.*;|~|@|\\&|\\+)?"+m_botNick[originalServerName]+"$").match(user).hasMatch()) {
continue;
}
QString onlineUser = HTML_ONLINE_POINT;
replaceTag(onlineUser, "NICKNAME", user);
onlineUserS += onlineUser;
currentOnline++;
}
replaceTag(page, "ONLINE", QString::number(currentOnline));
replaceTag(page, "ONLINE_LIST", onlineUserS);
if (not searchRequest.isEmpty()) {
page.replace("<input class=\"main_header__search_input\" type=\"search\" name=\"toSearch\" placeholder=\"{{SEARCH_PLACEHOLDER}}\">",
"<input class=\"main_header__search_input\" type=\"search\" name=\"toSearch\" value=\"" + searchRequest.toHtmlEscaped() + "\">");
} else if (middlePath == "<a style=\"text-decoration: none\" href=\"/"+server+"/"+channel+"\">" + "/" + "</a>") {
replaceTag(page, "SEARCH_PLACEHOLDER", originalChannelName);
} else if (month.isEmpty()) {
replaceTag(page, "SEARCH_PLACEHOLDER", originalChannelName + "/" + year);
} else {
replaceTag(page, "SEARCH_PLACEHOLDER", originalChannelName + "/" + year + "/" + month);
}
//// Main section body compilation
QString payloadBlock;
// Search request
if (not searchRequest.isEmpty()) {
uint counter = 0;
QRegularExpression userRgx(searchRequest, QRegularExpression::CaseInsensitiveOption);
bool rgxIsValid = false;
if (isRegexp and userRgx.isValid()) {
rgxIsValid = true;
}
consoleLog("Search request (" + server + "): " + searchRequest);
QStringList paths;
QDirIterator it(fsPath.path());
while (it.hasNext()) {
QString currentPath = it.next();
if (currentPath.endsWith(".") or currentPath.endsWith("..")) continue;
QString logFolder = m_dataFolder;
#ifdef WIN32
logFolder.replace('\\', '/');
#endif
QString server {currentPath}; // Folder wich is not server folder is ignored
server.remove(QRegularExpression("^"+logFolder));
server.remove(QRegularExpression("/.*$"));
bool serverIsOk = false;
for (const auto &srv: m_servers) {
if (global::toLowerAndNoSpaces(srv.first) == server) {
serverIsOk = true;
break;
}
}
if (not serverIsOk) continue;
QString currentChannel {currentPath}; // Folder wich is not channel folder is ignored
currentChannel.remove(QRegularExpression("^"+logFolder+"[^/]*/"));
currentChannel.remove(QRegularExpression("/.*$"));
bool channelIsOk = false; // Канал явно указан в конфиге
for (const auto &ch: m_servers[originalServerName]) {
QString searchChan {ch.first};
searchChan.remove('#');
if (searchChan == currentChannel) {
channelIsOk = true;
break;
}
}
if (not channelIsOk) continue;
paths.push_back(currentPath);
}
if (paths.isEmpty()) {
payloadBlock = HTML_PAYLOAD_ERROR;
replaceTag(payloadBlock, "ERROR_TITLE", "Not found");
replaceTag(payloadBlock, "ERROR_TEXT", "");
}
else {
std::map<QString, QStringList> matchedPathsAndMessages;
if (not month.isEmpty()) {
for (const auto& path: paths) {
if (not QRegularExpression("^.*[0-9]{2}\\.txt$").match(path).hasMatch()) continue;
QFile file(path);
if (not file.open(QIODevice::ReadOnly)) {
consoleLog("Error! I can't open log file " + fsPath.path());
continue;
}
QString buffer {file.readLine()};
while (not buffer.isEmpty()) {
removeBrakelineSymbols(buffer);
bool finded = false;
if (rgxIsValid) {
if (QRegularExpression(searchRequest, QRegularExpression::CaseInsensitiveOption).match(buffer).hasMatch()) {
finded = true;
}
} else {
if (buffer.contains(searchRequest, Qt::CaseInsensitive)) {
finded = true;
}
}
if (finded) {
std::pair<QString, QString> rawMessage = splitUserNameAndMessage(buffer);
if (rawMessage.first.isEmpty() or rawMessage.second.isEmpty()) {
buffer = file.readLine();
continue;
}
counter++;
QString message = HTML_PAYLOAD_LIST_CHAT_MESSAGE;
if (rawMessage.second == global::BLINDED_MESSAGE_MERKER) {
message.replace("class=\"main_payload__chat\"",
"class=\"main_payload__chat\" style=\"opacity: .5\"");
}
for (const auto &user: m_servers[originalServerName][originalChannelName]) {
if (QRegularExpression("^(.*;|~|@|\\&|\\+)?"+rawMessage.first+"$").match(user).hasMatch()) {
message.replace("class=\"main_payload__chat_username\"",
"class=\"main_payload__chat_username\" style=\"color: green\"");
break;
}
}
replaceTag(message, "USERNAME", rawMessage.first);
replaceTag(message, "MESSAGE_TEXT", rawMessage.second);
matchedPathsAndMessages[path].push_back(message);
}
buffer = file.readLine();
}
file.close();
}
}
else if (month.isEmpty() and not year.isEmpty()){
for (const auto &p: paths) {
QStringList slavePaths;
QDirIterator it(p);
while (it.hasNext()) {
QString fileName = it.next();
if (fileName.endsWith(".") or fileName.endsWith("..")) continue;
if (not QRegularExpression("\\.txt$").match(fileName).hasMatch()) continue;
slavePaths.push_back(fileName);
}
for (const auto &path: slavePaths) {
if (not QRegularExpression("^.*[0-9]{2}\\.txt$").match(path).hasMatch()) continue;
QFile file(path);
if (not file.open(QIODevice::ReadOnly)) {
consoleLog("Error! I can't open log file " + fsPath.path());
continue;
}
QString buffer {file.readLine()};
while (not buffer.isEmpty()) {
removeBrakelineSymbols(buffer);
bool finded = false;
if (rgxIsValid) {
if (QRegularExpression(searchRequest, QRegularExpression::CaseInsensitiveOption).match(buffer).hasMatch()) {
finded = true;
}
} else {
if (buffer.contains(searchRequest, Qt::CaseInsensitive)) {
finded = true;
}
}
if (finded) {
std::pair<QString, QString> rawMessage = splitUserNameAndMessage(buffer);
if (rawMessage.first.isEmpty() or rawMessage.second.isEmpty()) {
buffer = file.readLine();
continue;
}
counter++;
QString message = HTML_PAYLOAD_LIST_CHAT_MESSAGE;
if (rawMessage.second == global::BLINDED_MESSAGE_MERKER) {
message.replace("class=\"main_payload__chat\"",
"class=\"main_payload__chat\" style=\"opacity: .5\"");
}
for (const auto &user: m_servers[originalServerName][originalChannelName]) {
if (QRegularExpression("^^(.*;|~|@|\\&|\\+)?"+rawMessage.first+"$").match(user).hasMatch()) {
message.replace("class=\"main_payload__chat_username\"",
"class=\"main_payload__chat_username\" style=\"color: green\"");
break;
}
}
replaceTag(message, "USERNAME", rawMessage.first);
replaceTag(message, "MESSAGE_TEXT", rawMessage.second);
matchedPathsAndMessages[path].push_back(message);
}
buffer = file.readLine();
}
file.close();
}
}
}
else { // root directory
QStringList yearPaths;
for (const auto& p: paths) {
if (not QRegularExpression("/2[0-9]{3}$").match(p).hasMatch()) continue;
yearPaths.push_back(p);
}
QStringList fileNameS;
for (const auto& p: yearPaths) {
QStringList slavePaths;
QDirIterator it(p);
while (it.hasNext()) {
QString folderName = it.next();
if (folderName.endsWith(".") or folderName.endsWith("..")) continue;
if (not QRegularExpression("/[0-9]{2}$").match(folderName).hasMatch()) continue;
slavePaths.push_back(folderName);
}
for (const auto &path: slavePaths) {
QDirIterator itMonth(path);
while (itMonth.hasNext()) {
QString fileName = itMonth.next();
if (fileName.endsWith(".") or fileName.endsWith("..")) continue;
if (not QRegularExpression("^.*[0-9]{2}\\.txt$").match(fileName).hasMatch()) continue;
fileNameS.push_back(fileName);
}
}
}
for (const auto& path: fileNameS) {
QFile file(path);
if (not file.open(QIODevice::ReadOnly)) {
consoleLog("Error! I can't open log file " + fsPath.path());
continue;
}
QString buffer {file.readLine()};
while (not buffer.isEmpty()) {
removeBrakelineSymbols(buffer);
bool finded = false;
if (rgxIsValid) {
if (QRegularExpression(searchRequest, QRegularExpression::CaseInsensitiveOption).match(buffer).hasMatch()) {
finded = true;
}
} else {
if (buffer.contains(searchRequest, Qt::CaseInsensitive)) {
finded = true;
}
}
if (finded) {
std::pair<QString, QString> rawMessage = splitUserNameAndMessage(buffer);
if (rawMessage.first.isEmpty() or rawMessage.second.isEmpty()) {
buffer = file.readLine();
continue;
}
counter++;
QString message = HTML_PAYLOAD_LIST_CHAT_MESSAGE;
if (rawMessage.second == global::BLINDED_MESSAGE_MERKER) {
message.replace("class=\"main_payload__chat\"",
"class=\"main_payload__chat\" style=\"opacity: .5\"");
}
for (const auto &user: m_servers[originalServerName][originalChannelName]) {
if (QRegularExpression("^^(.*;|~|@|\\&|\\+)?"+rawMessage.first+"$").match(user).hasMatch()) {
message.replace("class=\"main_payload__chat_username\"",
"class=\"main_payload__chat_username\" style=\"color: green\"");
break;
}
}
replaceTag(message, "USERNAME", rawMessage.first);
replaceTag(message, "MESSAGE_TEXT", rawMessage.second);
matchedPathsAndMessages[path].push_back(message);
}
buffer = file.readLine();
}
file.close();
}
}
if (matchedPathsAndMessages.empty()) {
payloadBlock = HTML_PAYLOAD_ERROR;
replaceTag(payloadBlock, "ERROR_TITLE", "Not found");
replaceTag(payloadBlock, "ERROR_TEXT", "");
}
else {
QStringList findedPaths;
for (const auto& fp: matchedPathsAndMessages) {
findedPaths << fp.first;
}
std::sort(findedPaths.begin(), findedPaths.end());
for (auto& link: findedPaths) {
QString logFolder {m_dataFolder};
logFolder.remove(QRegularExpression(".$"));
QStringList& messages = matchedPathsAndMessages[link];
link.remove(logFolder);
link.remove(QRegularExpression("\\.txt$"));
QString finded = HTML_PAYLOAD_LIST_POINT_MESSAGE;
finded.replace("class=\"main_payload__block\"", "class=\"main_payload__block\" style=\"background: #b6c7d6\"");
replaceTag(finded, "POINT_LINK", link);
link.remove(QRegularExpression("^.*"+channel));
replaceTag(finded, "POINT_CONTENT", link);
payloadBlock += finded;
for(const auto& m: messages) {
payloadBlock += m;
}
payloadBlock += "&nbsp;\n";
}
}
}
int lastWbr = 0;
for (int i = 0; i < searchRequest.size(); i++) {
if (i-lastWbr > MAX_MESSAGE_LENGTH_WITHOUT_WBR) {
searchRequest.insert(i, "<wbr>");
lastWbr = i;
}
}
middlePath += "&nbsp;<span style=\"color: #4f9c65\">" + searchRequest.toHtmlEscaped() + "</span>&nbsp;";
if (rgxIsValid) middlePath += "rgx";
middlePath += "(" + QString::number(counter) + ")";
}
// Plain log explorer
else {
if (year.isEmpty()) { // /
QStringList folderNameS;
QDirIterator it(fsPath.path());
while (it.hasNext()) {
QString folderName = it.next();
if (folderName.endsWith(".") or folderName.endsWith("..")) continue;
while(folderName.contains('/')) {
folderName.remove(QRegularExpression("^.*/"));
}
folderName.remove(QRegularExpression("\\.txt$"));
folderNameS << folderName;
}
if (not folderNameS.isEmpty()) {
std::sort(folderNameS.begin(), folderNameS.end());
for (const auto &f: folderNameS) {
QString onePoint = HTML_PAYLOAD_LIST_POINT_FOLDER;
replaceTag(onePoint, "POINT_CONTENT", f);
replaceTag(onePoint, "POINT_LINK", "/"+server+"/"+channel+"/"+f);
payloadBlock += onePoint;
}
}
}
else if (not year.isEmpty() and month.isEmpty()) { // /YYYY
QStringList folderNameS;
QDirIterator it(fsPath.path());
while (it.hasNext()) {
QString folderName = it.next();
if (folderName.endsWith(".") or folderName.endsWith("..")) continue;
while(folderName.contains('/')) {
folderName.remove(QRegularExpression("^.*/"));
}
folderNameS << folderName;
}
if (not folderNameS.isEmpty()) {
std::sort(folderNameS.begin(), folderNameS.end());
for (const auto &f: folderNameS) {
QString onePoint = HTML_PAYLOAD_LIST_POINT_FOLDER;
bool yearIsOk {false};
bool monthIsOk {false};
QDate dateOfMonth (year.toInt(&yearIsOk), f.toInt(&monthIsOk), 1);
QString nameOfMonth;
if (yearIsOk and monthIsOk) {
nameOfMonth = QLocale().standaloneMonthName(dateOfMonth.month(), QLocale::ShortFormat);
}
QDirIterator dayLogs(fsPath.path() + global::slash + f);
int8_t dayLogsCounter {0};
while (dayLogs.hasNext()) {
QString dayLogFile = dayLogs.next();
if (dayLogFile.endsWith(".") or dayLogFile.endsWith("..")) continue;
dayLogsCounter++;
}
QString filesLabel;
dayLogsCounter == 1 ? filesLabel = "day" : filesLabel = "days";
replaceTag(onePoint, "POINT_CONTENT", "<span style=\"font-weight: bold\">" + f + "</span>&nbsp;(" + nameOfMonth + ") " + QString::number(dayLogsCounter) + " " + filesLabel);
replaceTag(onePoint, "POINT_LINK", "/"+server+"/"+channel+"/"+year+"/"+f);
payloadBlock += onePoint;
}
}
}
else if (not month.isEmpty() and day.isEmpty()) { // /YYYY/MM
QStringList fileNameS;
QDirIterator it(fsPath.path());
while (it.hasNext()) {
QString fileName = it.next();
if (fileName.endsWith(".") or fileName.endsWith("..")) continue; // QDir::NoDotAndNoDotDot not works!
while(fileName.contains('/')) {
fileName.remove(QRegularExpression("^.*/"));
}
fileName.remove(QRegularExpression("\\.txt$"));
fileNameS << fileName;
}
if (not fileNameS.isEmpty()) {
std::sort(fileNameS.begin(), fileNameS.end());
for (const auto &a: fileNameS) {
QString onePoint = HTML_PAYLOAD_LIST_POINT_MESSAGE;
bool yearIsOk {false};
bool monthIsOk {false};
bool dayIsOk {false};
QDate dateOfDay (year.toInt(&yearIsOk), month.toInt(&monthIsOk), a.toInt(&dayIsOk));
QString nameOfDay;
if (yearIsOk and monthIsOk and dayIsOk) {
nameOfDay = QLocale().standaloneDayName(dateOfDay.dayOfWeek(), QLocale::ShortFormat);
}
auto logFileSizeBytes = QFile(fsPath.path() + global::slash + a +".txt").size();
auto logFileSizeinKb = logFileSizeBytes/1000;
QString logFileSizeString;
if (logFileSizeinKb != 0) {
logFileSizeString = QString::number(logFileSizeinKb)+"."+
QString::number((logFileSizeBytes - logFileSizeinKb*1000)/10)+
" KB";
} else {
logFileSizeString = QString::number(logFileSizeBytes)+" B";
}
replaceTag(onePoint, "POINT_CONTENT", "<span style=\"font-weight: bold\">" + a + "</span>&nbsp;(" + nameOfDay + ") " + logFileSizeString);
replaceTag(onePoint, "POINT_LINK", "/"+server+"/"+channel+"/"+year+"/"+month+"/"+a);
payloadBlock += onePoint;
}
}
}
else if (not day.isEmpty()) { // /YYYY/MM/dd
QFile file(fsPath.path()+global::slash+day+".txt");
if (not file.open(QIODevice::ReadOnly)) {
consoleLog("Error! I can't open log file " + fsPath.path());
payloadBlock = HTML_PAYLOAD_ERROR;
replaceTag(payloadBlock, "ERROR_TITLE", "Internal error");
replaceTag(payloadBlock, "ERROR_TEXT", "Requested log file openning failed");
}
else {
middlePath += "/" + day;
QString buffer = file.readLine();
while (not buffer.isEmpty()) {
removeBrakelineSymbols(buffer);
std::pair<QString, QString> rawMessage = splitUserNameAndMessage(buffer);
if (rawMessage.first.isEmpty() or rawMessage.second.isEmpty()) {
buffer = file.readLine();
continue;
}
QString message = HTML_PAYLOAD_LIST_CHAT_MESSAGE;
if (rawMessage.second == global::BLINDED_MESSAGE_MERKER) {
message.replace("class=\"main_payload__chat\"",
"class=\"main_payload__chat\" style=\"opacity: .5\"");
}
for (const auto &user: m_servers[originalServerName][originalChannelName]) {
if (QRegularExpression("^(.*;|~|@|\\&|\\+)?"+rawMessage.first+"$").match(user).hasMatch()) {
message.replace("class=\"main_payload__chat_username\"",
"class=\"main_payload__chat_username\" style=\"color: green\"");
break;
}
}
replaceTag(message, "USERNAME", rawMessage.first);
replaceTag(message, "MESSAGE_TEXT", rawMessage.second);
payloadBlock += message;
buffer = file.readLine();
}
file.close();
// arrows to prev & next days
bool prevDayFound {false};
uint prevDayUInt {day.toUShort()};
uint prevMonthUInt {month.toUShort()};
uint prevYearUInt {year.toUShort()};
QDir fsPathForPrev {fsPath};
for(uint i = prevDayUInt-1; i >= 1; i--) { // current month
QString d {fsPathForPrev.path()+global::slash};
if (i < 10) d += "0";
d += QString::number(i)+".txt";
if (QFile::exists(d)) {
prevDayUInt = i;
prevDayFound = true;
break;
}
}
if (not prevDayFound) { // other month
fsPathForPrev.cdUp();
bool mf {false};
for (uint i = prevMonthUInt-1; i >= 1; i--) {
QString m;
if (i < 10) m += "0";
m += QString::number(i);
if (fsPathForPrev.cd(m)) {
prevMonthUInt = i;
mf = true;
break;
}
}
if (mf) {
for(uint i = 31; i >= 1; i--) {
QString d {fsPathForPrev.path()+global::slash};
if (i < 10) d += "0";
d += QString::number(i)+".txt";
if (QFile::exists(d)) {
prevDayUInt = i;
prevDayFound = true;
break;
}
}
}
}
if (not prevDayFound) { // other year
fsPathForPrev.cdUp();
bool yf {false};
for (uint i = prevYearUInt-1; i > prevYearUInt-21; i--) {
if (fsPathForPrev.cd(QString::number(i))) {
prevYearUInt = i;
yf = true;
break;
}
}
if (yf) {
bool mf {false};
for (uint i = 12; i >= 1; i--) {
QString m;
if (i < 10) m += "0";
m += QString::number(i);
if (fsPathForPrev.cd(m)) {
prevMonthUInt = i;
mf = true;
break;
}
}
if (mf) {
for(uint i = 31; i >= 1; i--) {
QString d {fsPathForPrev.path()+global::slash};
if (i < 10) d += "0";
d += QString::number(i)+".txt";
if (QFile::exists(d)) {
prevDayUInt = i;
prevDayFound = true;
break;
}
}
}
}
}
QString leftArrow;
if (prevDayFound) {
leftArrow = HTML_PAYLOAD_MIDDLEPATH_ARROW_PREV;
QString link = "/"+server+"/"+channel+"/"+QString::number(prevYearUInt)+"/";
QString m;
if (prevMonthUInt < 10) m += "0";
m += QString::number(prevMonthUInt);
link += m+"/";
QString d;
if (prevDayUInt < 10) d += "0";
d += QString::number(prevDayUInt);
link += d;
replaceTag(leftArrow, "LINK", link);
} else {
leftArrow = HTML_PAYLOAD_MIDDLEPATH_ARROW_PREV_FALSE;
}
middlePath += leftArrow;
bool nextDayFound {false};
uint nextDayUInt {day.toUShort()};
uint nextMonthUInt {month.toUShort()};
uint nextYearUInt {year.toUShort()};
QDir fsPathForNext {fsPath};
for(uint i = nextDayUInt+1; i <= 31; i++) { // current month
QString d {fsPathForNext.path()+global::slash};
if (i < 10) d += "0";
d += QString::number(i)+".txt";
if (QFile::exists(d)) {
nextDayUInt = i;
nextDayFound = true;
break;
}
}
if (not nextDayFound) { // other month
fsPathForNext.cdUp();
bool mf {false};
for (uint i = nextMonthUInt+1; i <= 12; i++) {
QString m;
if (i < 10) m += "0";
m += QString::number(i);
if (fsPathForNext.cd(m)) {
nextMonthUInt = i;
mf = true;
break;
}
}
if (mf) {
for(uint i = 1; i <= 31; i++) {
QString d {fsPathForNext.path()+global::slash};
if (i < 10) d += "0";
d += QString::number(i)+".txt";
if (QFile::exists(d)) {
nextDayUInt = i;
nextDayFound = true;
break;
}
}
}
}
if (not nextDayFound) { // other year
fsPathForNext.cdUp();
bool yf {false};
for (uint i = nextYearUInt+1; i < nextYearUInt+20; i++) {
if (fsPathForNext.cd(QString::number(i))) {
nextYearUInt = i;
yf = true;
break;
}
}
if (yf) {
bool mf {false};
for (uint i = 1; i <= 12; i++) {
QString m;
if (i < 10) m += "0";
m += QString::number(i);
if (fsPathForNext.cd(m)) {
nextMonthUInt = i;
mf = true;
break;
}
}
if (mf) {
for(uint i = 1; i <= 31; i++) {
QString d {fsPathForNext.path()+global::slash};
if (i < 10) d += "0";
d += QString::number(i)+".txt";
if (QFile::exists(d)) {
nextDayUInt = i;
nextDayFound = true;
break;
}
}
}
}
}
QString rightArrow;
if (nextDayFound) {
rightArrow = HTML_PAYLOAD_MIDDLEPATH_ARROW_NEXT;
QString link = "/"+server+"/"+channel+"/"+QString::number(nextYearUInt)+"/";
QString m;
if (nextMonthUInt < 10) m += "0";
m += QString::number(nextMonthUInt);
link += m+"/";
QString d;
if (nextDayUInt < 10) d += "0";
d += QString::number(nextDayUInt);
link += d;
replaceTag(rightArrow, "LINK", link);
} else {
rightArrow = HTML_PAYLOAD_MIDDLEPATH_ARROW_NEXT_FALSE;
}
middlePath += rightArrow;
}
}
if (payloadBlock.isEmpty()) {
payloadBlock = HTML_PAYLOAD_ERROR;
replaceTag(payloadBlock, "ERROR_TITLE", "Empty");
replaceTag(payloadBlock, "ERROR_TEXT", "No logs found for this channel");
}
}
replaceTag(page, "MIDDLE_PATH", middlePath);
replaceTag(page, "PAYLOAD_BLOCK", payloadBlock);
//// Footer
replaceTag(page, "VERSION", IRCABOT_VERSION);
replaceTag(page, "COPYRIGHT_YEAR", COPYRIGHT_YEAR);
replaceTag(page, "RENDERING_TIMER", QString::number(QDateTime::currentMSecsSinceEpoch() - renderStart));
QString mainHeader = HEADER_HTML;
replaceTag(mainHeader, "SIZE", QString::number(QByteArray(page.toUtf8()).size()));
if (socket->isOpen()) {
socket->write(mainHeader.toUtf8());
if (not isHeadRequest) socket->write(page.toUtf8());
}
}
void HttpServer::writeRealTimeChatPage(QTcpSocket *socket, QString &urlPath, bool isHeadRequest)
{
if (isHeadRequest) {
QString header = HEADER_HTML;
replaceTag(header, "SIZE", "0");
socket->write(header.toUtf8());
return;
}
auto renderStart = QDateTime::currentMSecsSinceEpoch();
urlPath.remove(QRegularExpression("^/realtimereadingchat/"));
QString server = getWordFromPath(urlPath);
if (server.isEmpty()) {
writeErrorPage(socket, "SERVER VALUE IS MISSING");
return;
}
urlPath.remove(QRegularExpression("^"+server));
QString channel = getWordFromPath(urlPath);
if (channel.isEmpty()) {
writeErrorPage(socket, "CHANNEL VALUE IS MISSING");
return;
}
QString originalServerName;
for (const auto &s: m_servers) {
if (global::toLowerAndNoSpaces(s.first) == server) {
originalServerName = s.first;
}
}
if (originalServerName.isEmpty()) {
writeErrorPage(socket, "REQUESTED SERVER NOT EXIST");
return;
}
QString originalChannelName;
for (const auto &server: m_servers) {
for (const auto &channel_users: server.second) {
if (global::toLowerAndNoSpaces(channel_users.first) == "#"+channel) {
originalChannelName = global::toLowerAndNoSpaces(channel_users.first);
}
}
}
if (originalChannelName.isEmpty()) {
writeErrorPage(socket, "REQUESTED CHANNEL NOT EXIST");
return;
}
QFile main("://html/main.html");
QString page;
if (main.open(QIODevice::ReadOnly)) {
page = main.readAll();
main.close();
}
else {
if (socket->isOpen()) {
socket->write(HEADER_404.toUtf8());
if (not isHeadRequest) socket->write(CRITICAL_ERROR);
}
}
QString year {QDateTime::currentDateTime().toString("yyyy")};
QString month {QDateTime::currentDateTime().toString("MM")};
QString day {QDateTime::currentDateTime().toString("dd")};
//// Left menu compilation
QString htmlServersSectionS;
for (const auto &s: m_servers) {
if (s.first.isEmpty()) continue; // empty server name?
QString htmlServersSection = HTML_SERVER_SECTION;
replaceTag(htmlServersSection, "ABOUT_SERVER", "/"+server);
replaceTag(htmlServersSection, "SERVER_NAME", s.first);
if (s.first == originalServerName) {
htmlServersSection.replace("<span style=\"font-size: 17px;\">{{ONLINE_STATUS}}",
"<span style=\"font-size: 17px;\" id=\"serverStatus\">{{ONLINE_STATUS}}");
}
QString htmlChannelLineS;
for (const auto &c: s.second) {
QString htmlChannelLine;
if (originalServerName == s.first and originalChannelName == c.first) {
htmlChannelLine = HTML_SERVER_SECTION_CHANNEL_SELECTED;
} else {
htmlChannelLine = HTML_SERVER_SECTION_CHANNEL;
}
replaceTag(htmlChannelLine, "CHANNEL_NAME", c.first);
QString channelNameForUrl {c.first};
channelNameForUrl.remove('#');
QString channelLink = "/realtimereadingchat/" + global::toLowerAndNoSpaces(s.first) + "/" + channelNameForUrl;
replaceTag(htmlChannelLine, "CHANNEL_LINK", channelLink);
htmlChannelLineS += htmlChannelLine;
}
replaceTag(htmlServersSection, "CHANNELS", htmlChannelLineS);
bool online {false};
for (const auto &srv: m_serversOnline) {
if (srv.first == s.first) {
online = srv.second;
break;
}
}
if (online) {
replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_ONLINE_MARKER);
} else {
replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_OFFLINE_MARKER);
}
if (s.first == originalServerName) {
htmlServersSectionS.push_front(htmlServersSection);
} else {
htmlServersSectionS += htmlServersSection;
}
}
htmlServersSectionS.push_front(m_serviceButton);
replaceTag(page, "SERVERS_SECTION", htmlServersSectionS);
//// Main section header compilation
QString& topic = m_channelsTopic[originalServerName][originalChannelName];
topic = topic.replace('\"', "&quot;");
QString titlePostfix = " | " + m_serviceName;
if (not topic.isEmpty()) {
titlePostfix.push_front(" | " + topic);
}
if (m_servers.size() > 1) {
replaceTag(page, "PAGE_TITLE", originalChannelName + " ("+originalServerName+") [real time]" + titlePostfix);
} else {
replaceTag(page, "PAGE_TITLE", originalChannelName + " [real time]" + titlePostfix);
}
replaceTag(page, "CHANNEL_TOPIC", topic);
replaceTag(page, "MAIN_HEADER", originalChannelName);
replaceTag(page, "ADDITIONAL_BUTTON", HTML_PAYLOAD_ADDITIONAL_NOTIFY);
replaceTag(page, "REALTIME_LINK", "/"+server+"/"+channel+"/"+year+"/"+month+"/"+day);
replaceTag(page, "AIRPLAIN_TITLE", "Back to plain text log");
page.replace("class=\"main_header__title_airplaine\"", "class=\"main_header__title_airplaine\" style=\"transform: scale(-1,1)\"");
int currentOnline = 0;
QString onlineUserS;
for (const auto &user: m_servers[originalServerName][originalChannelName]) {
if (QRegularExpression("^(.*;|~|@|\\&|\\+)?"+m_botNick[originalServerName]+"$").match(user).hasMatch()) {
continue;
}
QString onlineUser = HTML_ONLINE_POINT;
replaceTag(onlineUser, "NICKNAME", user);
onlineUserS += onlineUser;
currentOnline++;
}
page.replace("{{ONLINE}}", "<span id=\"online\">{{ONLINE}}</span>");
replaceTag(page, "ONLINE", QString::number(currentOnline));
page.replace("<div class=\"main_middle__online_list\">", "<div id=\"onlineList\" class=\"main_middle__online_list\">");
replaceTag(page, "ONLINE_LIST", onlineUserS);
page.replace("class=\"main_header__search_form\"", "action=\"/"+server+"/"+channel+"\" class=\"main_header__search_form\"");
replaceTag(page, "SEARCH_PLACEHOLDER", originalChannelName);
page.replace("<div class=\"main_middle__path\">", "<div class=\"main_middle__path\" id=\"path\">");
replaceTag(page, "MIDDLE_PATH", "");
//// Payload
page.replace("<div class=\"main_payload\">", "<div class=\"main_payload\" id=\"payload\">");
bool logsExisted {false};
QFile file;
QDir fsPath(m_dataFolder+server+global::slash+channel);
if (fsPath.cd(year)) {
if (fsPath.cd(month)) {
file.setFileName(fsPath.path()+global::slash+day+".txt");
if (file.open(QIODevice::ReadOnly)) {
logsExisted = true;
}
}
}
QString payloadBlock;
if (logsExisted) {
QString buffer = file.readLine();
while (not buffer.isEmpty()) {
removeBrakelineSymbols(buffer);
std::pair<QString, QString> rawMessage = splitUserNameAndMessage(buffer);
if (rawMessage.first.isEmpty() or rawMessage.second.isEmpty()) {
buffer = file.readLine();
continue;
}
QString message = HTML_PAYLOAD_LIST_CHAT_MESSAGE;
if (rawMessage.second == global::BLINDED_MESSAGE_MERKER) {
message.replace("class=\"main_payload__chat\"",
"class=\"main_payload__chat\" style=\"opacity: .5\"");
}
message.replace("class=\"main_payload__chat_username\"",
"class=\"main_payload__chat_username\" style=\"color: #e34f10\"");
replaceTag(message, "USERNAME", rawMessage.first);
replaceTag(message, "MESSAGE_TEXT", rawMessage.second);
payloadBlock += message;
buffer = file.readLine();
}
file.close();
}
QString hr = HTML_PAYLOAD_LIST_CHAT_MESSAGE;
hr.replace("class=\"main_payload__chat_mail\"", "id=\"hr\" class=\"main_payload__chat_mail\"");
hr.replace("class=\"main_payload__chat_username\"",
"class=\"main_payload__chat_username\" style=\"color: white; text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;\"");
replaceTag(hr, "USERNAME", "IRCaBot");
replaceTag(hr, "MESSAGE_TEXT", "<b>New messages won't show without JavaScript.</b><br>"
"My JS code is small and simple. Check it at "
"<a href=\"/realtimechat.js\">/realtimechat.js</a> and come back "
"with enabled!");
payloadBlock += hr;
replaceTag(page, "PAYLOAD_BLOCK", payloadBlock);
//// Footer
replaceTag(page, "VERSION", IRCABOT_VERSION);
replaceTag(page, "COPYRIGHT_YEAR", COPYRIGHT_YEAR);
//// Finish
page.replace("</body>", " <div id=\"LMId\" style=\"display: none\">" + QString::number(QDateTime::currentMSecsSinceEpoch()) + "</div>\n"
" <div id=\"ajaxPath\" style=\"display: none\">" + server + "/" + channel + "</div>\n"
" <script src=\"/realtimechat.js\"></script>\n</body>");
replaceTag(page, "RENDERING_TIMER", QString::number(QDateTime::currentMSecsSinceEpoch() - renderStart));
QString mainHeader = HEADER_HTML;
replaceTag(mainHeader, "SIZE", QString::number(QByteArray(page.toUtf8()).size()));
if (socket->isOpen()) {
socket->write(mainHeader.toUtf8());
if (not isHeadRequest) socket->write(page.toUtf8());
}
}
void HttpServer::writeAboutServerPage(QTcpSocket *socket, QString &server, bool isHeadRequest)
{
auto renderStart = QDateTime::currentMSecsSinceEpoch();
QString originalServerName;
for (const auto &s: m_servers) {
if (global::toLowerAndNoSpaces(s.first) == server) {
originalServerName = s.first;
}
}
if (originalServerName.isEmpty()) {
writeErrorPage(socket, "SERVER NOT EXIST");
return;
}
QFile main("://html/main.html");
QString page;
if (main.open(QIODevice::ReadOnly)) {
page = main.readAll();
main.close();
}
else {
if (socket->isOpen()) {
socket->write(HEADER_404.toUtf8());
if (not isHeadRequest) socket->write(CRITICAL_ERROR);
}
}
replaceTag(page, "PAGE_TITLE", "About " + originalServerName + " | " + m_serviceName);
//// Left menu compilation
QString htmlServersSectionS;
for (const auto &s: m_servers) {
if (s.first.isEmpty()) continue; // empty server name?
QString htmlServersSection = HTML_SERVER_SECTION;
if (s.first == originalServerName) {
htmlServersSection.replace("<div class=\"left_menu__item\">",
"<div class=\"left_menu__item\" style=\"background: #f0f5fa\">");
}
replaceTag(htmlServersSection, "ABOUT_SERVER", "/"+s.first);
replaceTag(htmlServersSection, "SERVER_NAME", s.first);
QString htmlChannelLineS;
for (const auto &c: s.second) {
QString htmlChannelLine;
htmlChannelLine = HTML_SERVER_SECTION_CHANNEL;
replaceTag(htmlChannelLine, "CHANNEL_NAME", c.first);
QString channelNameForUrl {c.first};
channelNameForUrl.remove('#');
QString channelLink = "/" + global::toLowerAndNoSpaces(s.first) + "/" + channelNameForUrl;
replaceTag(htmlChannelLine, "CHANNEL_LINK", channelLink);
htmlChannelLineS += htmlChannelLine;
}
replaceTag(htmlServersSection, "CHANNELS", htmlChannelLineS);
bool online {false};
for (const auto &srv: m_serversOnline) {
if (srv.first == s.first) {
online = srv.second;
break;
}
}
if (online) {
replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_ONLINE_MARKER);
} else {
replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_OFFLINE_MARKER);
}
if (s.first == originalServerName) {
htmlServersSectionS.push_front(htmlServersSection);
} else {
htmlServersSectionS += htmlServersSection;
}
}
htmlServersSectionS.push_front(m_serviceButton);
replaceTag(page, "SERVERS_SECTION", htmlServersSectionS);
page.remove(QRegularExpression("<div class=\"main_header\">.*<!-- main_middle -->", QRegularExpression::DotMatchesEverythingOption));
QString payloadBlock = HTML_PAYLOAD_ABOUT;
replaceTag(payloadBlock, "ABOUT_TITLE", originalServerName);
QString aboutBlock;
QFile about(m_dataFolder+server+global::slash+"about_server.txt");
if (about.open(QIODevice::ReadOnly)) {
QString rbuffer = about.readLine();
while (not rbuffer.isEmpty()) {
if (rbuffer.startsWith('#')) {
rbuffer = about.readLine();
continue;
}
removeBrakelineSymbols(rbuffer);
if (not rbuffer.isEmpty()) {
aboutBlock += rbuffer;
}
rbuffer = about.readLine();
}
}
else {
aboutBlock = "No information provided";
}
replaceTag(payloadBlock, "ABOUT_TEXT", aboutBlock);
replaceTag(page, "PAYLOAD_BLOCK", payloadBlock);
//// Footer
replaceTag(page, "VERSION", IRCABOT_VERSION);
replaceTag(page, "COPYRIGHT_YEAR", COPYRIGHT_YEAR);
//// Finish
replaceTag(page, "RENDERING_TIMER", QString::number(QDateTime::currentMSecsSinceEpoch() - renderStart));
QString mainHeader = HEADER_HTML;
replaceTag(mainHeader, "SIZE", QString::number(QByteArray(page.toUtf8()).size()));
if (socket->isOpen()) {
socket->write(mainHeader.toUtf8());
if (not isHeadRequest) socket->write(page.toUtf8());
}
}
void HttpServer::writeAjaxAnswer(QTcpSocket *socket, QString &urlPath, bool isHeadRequest)
{
if (isHeadRequest) {
QString header = HEADER_JSON;
replaceTag(header, "SIZE", "0");
socket->write(header.toUtf8());
return;
}
//// Validate
urlPath.remove(QRegularExpression("^/ajax/"));
QString server = getWordFromPath(urlPath);
if (server.isEmpty()) {
writeErrorJson(socket, "Invalid request. Server value is missing!");
return;
}
urlPath.remove(QRegularExpression("^"+server));
QString channel = getWordFromPath(urlPath);
if (channel.isEmpty()) {
writeErrorJson(socket, "Invalid request. Channel value is missing!");
return;
}
QString originalServerName;
for (const auto &s: m_servers) {
if (global::toLowerAndNoSpaces(s.first) == server) {
originalServerName = s.first;
}
}
if (originalServerName.isEmpty()) {
writeErrorJson(socket, "Invalid request. Server not exist!");
return;
}
QString originalChannelName;
for (const auto &server: m_servers) {
for (const auto &channel_users: server.second) {
if (global::toLowerAndNoSpaces(channel_users.first) == "#"+channel) {
originalChannelName = global::toLowerAndNoSpaces(channel_users.first);
}
}
}
if (originalChannelName.isEmpty()) {
writeErrorJson(socket, "Invalid request. Channel not exist!");
return;
}
//// Parse
bool userOnlineCounterIsOk {false};
auto userOnlineCounter = global::getValue(urlPath, "onlineCounter", global::eForWeb).toInt(&userOnlineCounterIsOk);
if (not userOnlineCounterIsOk) {
writeErrorJson(socket, "Invalid request: 'onlineCounter' (int) value is missing!");
return;
}
bool userMessageLastIdIsOk {false};
qint64 userMessageLastId = global::getValue(urlPath, "messageId", global::eForWeb).toLongLong(&userMessageLastIdIsOk);
if (not userMessageLastIdIsOk) {
writeErrorJson(socket, "Invalid request: 'messageId' value is missing!");
return;
}
bool userServerStatus = global::getValue(urlPath, "serverStatus", global::eForWeb) == "true";
//// Building
QJsonObject jResponse;
jResponse.insert("status", QJsonValue(true));
// online server
if (userServerStatus != m_serversOnline[originalServerName]) {
jResponse.insert("serverStatusChanged", QJsonValue(true));
jResponse.insert("serverStatus", QJsonValue(m_serversOnline[originalServerName]));
} else {
jResponse.insert("serverStatusChanged", QJsonValue(false));
}
// online users
if (m_servers[originalServerName][originalChannelName].size()-1/*self*/ != userOnlineCounter) {
jResponse.insert("onlineUsersChanged", QJsonValue(true));
QJsonObject jOnline;
int currentOnline = m_servers[originalServerName][originalChannelName].size();
if (currentOnline > 0) currentOnline -= 1;
jOnline.insert("count", QJsonValue(currentOnline));
QJsonArray jOnlineList;
for (const auto& user: m_servers[originalServerName][originalChannelName]) {
if (QRegularExpression("^(.*;|~|@|\\&|\\+)?"+m_botNick[originalServerName]+"$").match(user).hasMatch()) {
continue;
}
jOnlineList.push_back(QJsonValue(user));
}
jOnline.insert("list", jOnlineList);
jResponse.insert("online", jOnline);
} else {
jResponse.insert("onlineUsersChanged", QJsonValue(false));
}
// new messages
bool newMessagesIsExisted {false};
QString channelId {server+channel};
QJsonArray jNewMessages;
qint64 newLastMessageId {userMessageLastId};
if (not m_messageCache.contains(channelId)) {
m_messageCache.insert(channelId, new MessagePool);
consoleLog("Message caching enabled for "+server+"/#"+channel+". Real time reading started.");
}
else {
auto messages = *(m_messageCache[channelId]->getMessages());
for (const auto& msg: messages) {
if (userMessageLastId < msg.first) {
if (not newMessagesIsExisted) newMessagesIsExisted = true;
QJsonObject jOneNewMessage;
jOneNewMessage.insert("user", msg.second->getSender());
jOneNewMessage.insert("text", msg.second->getText());
jNewMessages.push_back(jOneNewMessage);
newLastMessageId = msg.first;
}
}
}
if (newMessagesIsExisted) {
jResponse.insert("LMIdChanged" /* last message id == LMId */, QJsonValue(true));
jResponse.insert("newMessages", jNewMessages);
jResponse.insert("LMId", QJsonValue(QString::number(newLastMessageId)));
} else {
jResponse.insert("LMIdChanged", QJsonValue(false));
}
//// Finish
QByteArray response {QJsonDocument(jResponse).toJson()};
QString header = HEADER_JSON;
replaceTag(header, "SIZE", QString::number(response.size()));
socket->write(header.toUtf8());
socket->write(response);
}
/*\
|*| //////////'''''''''''''\\\\\\\\\\
|*| HAPPY NEW YEAR IN RUSSIAN PRISON!
|*| |||||||||| 2022 ||||||||||
|*| \\\\\\\\\\_____________//////////
\*/
Message::Message(const QString& s, const QString& t, qint64 timestamp, QObject *parent) :
QObject(parent),
m_sender(s),
m_text(t),
m_timestamp(timestamp)
{
connect (&m_selfKiller, &QTimer::timeout, [&](){emit outDated(m_timestamp);});
m_selfKiller.setSingleShot(true);
m_selfKiller.start(MSECS_TO_AUTOREMOVE_MESSAGES_FROM_BUFFER);
}
const QString Message::getSender()
{
return m_sender;
}
const QString Message::getText()
{
return m_text;
}
MessagePool::MessagePool(QObject *parent) :
QObject(parent),
m_lastPing(QDateTime::currentMSecsSinceEpoch())
{}
void MessagePool::saveNewMessage(const QString &nick, const QString &text)
{
qint64 timestamp = QDateTime::currentMSecsSinceEpoch();
Message* newMessage = new Message (nick, text, timestamp);
connect (newMessage, SIGNAL(outDated(qint64)), this, SLOT (messageToDelete(qint64)));
m_messages.insert({timestamp, newMessage});
}
const std::multimap<qint64, Message *>* MessagePool::getMessages()
{
m_lastPing = QDateTime::currentMSecsSinceEpoch();
return &m_messages;
}
qint64 MessagePool::getLastPing()
{
return m_lastPing;
}
void MessagePool::messageToDelete(qint64 timestamp)
{
while (m_messages.find(timestamp) != m_messages.end()) {
auto it = m_messages.find(timestamp);
it->second->deleteLater();
m_messages.erase(it);
}
}