#include "ircclient.h" #include "global.h" #include #include #include #include #include IrcClient::IrcClient(const ConnectionData& config, QObject *parent) : QObject(parent), m_socket(nullptr), m_connectionData(config), m_reconnectReport(false), m_connected(false), m_triggersIsStopped(false), m_shouldAppendMessage(false) { if (m_connectionData.address.isEmpty()) { throw std::runtime_error("Empty ConnectionData::address! You trying wrong way to use it, man!"); } QString path {global::toLowerAndNoSpaces(m_connectionData.displayName)}; QDir dir(m_connectionData.logFolderPath); if (not dir.exists()) { dir.cdUp(); dir.mkdir(path); } dir.cd(path); m_connectionData.channels.removeAll(""); for (auto chan: m_connectionData.channels) { chan.remove('#'); dir.mkdir(chan); } if (not QFile::exists(dir.path()+global::slash+"about_server.txt")) { QFile about(dir.path()+global::slash+"about_server.txt"); if (about.open(QIODevice::WriteOnly)) { about.write("# Server description file.\n" "# HTML is supported. For line breaks, use
.\n\n" "
¯\\_(ツ)_/¯
\n"); about.close(); } else { errorLog("Creating "+dir.path()+global::slash+"about_server.txt is failed"); } } connect (&m_pingTimeout, &QTimer::timeout, this, &IrcClient::pingTimedOut); connect (&m_usersActualize, &QTimer::timeout, this, &IrcClient::actualizeUsersList); connect (&m_nickRecover, &QTimer::timeout, this, &IrcClient::nickRecover); connect (&m_timerToJoin, &QTimer::timeout, this, &IrcClient::onLogin); connect (&m_stopWriting, &QTimer::timeout, this, [&](){m_triggersIsStopped = false;}); m_stopWriting.setSingleShot(true); } IrcClient::~IrcClient() { m_socket->write("QUIT IRCaBot " + global::IRCABOT_VERSION.toUtf8() + ": Disconnected by admin"); m_socket->disconnectFromHost(); } QStringList IrcClient::getOnlineUsers(const QString &channel) { if (m_online.find(channel) == m_online.end()) { return QStringList(); } return m_online[channel]; } void IrcClient::connectToServer() { if (not m_reconnectReport) { emit myNickname(m_connectionData.displayName, m_connectionData.nick); emit myOnline(m_connectionData.displayName, m_connected); emit startInfo(m_connectionData.displayName, m_connectionData.channels); } m_socket = new QTcpSocket; m_socket->connectToHost(m_connectionData.address, m_connectionData.port); if (not m_socket->waitForConnected()) { if (not m_reconnectReport) { consoleLog("Connection failed. Reconnecting..."); m_reconnectReport = true; } QThread::sleep(THREAD_SLEEP_ON_RECONNECT_TIMER); connectToServer(); return; } if (m_reconnectReport) { m_reconnectReport = false; } connect (m_socket, &QTcpSocket::disconnected, this, &IrcClient::onDisconnected); connect (m_socket, &QTcpSocket::readyRead, this, &IrcClient::onRead); write("USER " + m_connectionData.user + " . . " + m_connectionData.realName); write("NICK " + m_connectionData.nick); } void IrcClient::write(const QString &message, bool log) { if (m_socket == nullptr) { consoleLog("IrcClient::write() Socket is nullptr!"); return; } if (log) { consoleLog("<- " + message); } m_socket->write(message.toUtf8() + '\n'); if (not m_socket->waitForBytesWritten()) { consoleLog("IrcClient::write() Bytes was not written to socket!"); return; } } IrcClient::IrcCode IrcClient::getServerCode(const QString &message) { IrcCode code = IrcCode::Failed; QString result {message}; int beginPosition = result.indexOf(' '); if (beginPosition == -1) return code; result.remove(0, beginPosition+1); int endPosition = result.indexOf(' '); if (endPosition == -1) return code; result.remove(endPosition, result.size()-endPosition); bool convert {false}; int resultInt {result.toInt(&convert)}; if (not convert) return code; switch (resultInt) { case 332: code = IrcCode::ChannelTopic; break; case 353: code = IrcCode::NamesList; break; case 366: code = IrcCode::EndOfNamesList; break; case 433: code = IrcCode::NickNameIsAlreadyInUse; break; } return code; } QString IrcClient::getChannelName(const QString &message) { int begin = message.indexOf('#'); if (begin == -1) return QString(); QString result {message}; result.remove(0,begin); int end = result.indexOf(' '); if (end == -1) { end = result.size(); } else { result.remove(end, result.size()-end); } return result; } QString IrcClient::getNickname(const QString &message) { if (not message.startsWith(':')) return QString(); QString result {message}; result.remove(0,1); result.remove(QRegularExpression("!.*$")); if (result.contains(' ')) return QString(); return result; } void IrcClient::toTrigger(const QString& channel, const QString &nickname, const QString &message) { if (m_connectionData.triggers.isEmpty()) return; if (m_triggersIsStopped) { consoleLog("IrcClient::toTrigger() Trigger request is ignored (anti DDoS)"); return; } m_triggersIsStopped = true; m_stopWriting.start(WRITING_STOP_TIMER); for (auto trigger: m_connectionData.triggers) { if (message.contains(m_connectionData.triggers.key(trigger), Qt::CaseInsensitive)) { QString response {trigger}; response.replace(TRIGGER_CHANNEL_FOR_URL_PATTERN, global::toLowerAndNoSpaces(m_connectionData.displayName) + "/" + channel.mid(1, channel.size()-1)); write("PRIVMSG " + channel + " " + nickname + ", " + response); return; } } QString possibleTriggers; for (auto trigger: m_connectionData.triggers) { possibleTriggers += "'" + m_connectionData.triggers.key(trigger) + "', "; } possibleTriggers.replace(QRegularExpression(",\\s$"), "."); write("PRIVMSG " + channel + " " + nickname + ", try it: " + possibleTriggers); } void IrcClient::toChatLog(QString channel, const QString &nick, const QString &message) { QString msg {message}; // Empty message ignored msg.remove(" "); msg.remove("\t"); if (msg.isEmpty()) return; channel.remove('#'); emit newMessage(m_connectionData.displayName, channel, nick, message); QDir dir(m_connectionData.logFolderPath); if (not dir.exists(channel)) { dir.mkdir(channel); } if (not dir.cd(channel)) { errorLog("Can't open log folder (1) for #" + channel + " (" + nick + "): " + message); return; } QString year {QDateTime::currentDateTime().toString("yyyy")}; if (not dir.cd(year)) { if (not dir.mkdir(year)) { errorLog("Can't create log folder (2) for #" + channel + " (" + nick + "): " + message); return; } if (not dir.cd(year)) { errorLog("Can't open log folder (2) for #" + channel + " (" + nick + "): " + message); return; } } QString month {QDateTime::currentDateTime().toString("MM")}; if (not dir.cd(month)) { if (not dir.mkdir(month)) { errorLog("Can't create log folder (3) for #" + channel + " (" + nick + "): " + message); return; } if (not dir.cd(month)) { errorLog("Can't open log folder (3) for #" + channel + " (" + nick + "): " + message); return; } } QString day {QDateTime::currentDateTime().toString("dd")}; QFile file (dir.path() + global::slash + day + ".txt"); if (not file.open(QIODevice::WriteOnly | QIODevice::Append)) { errorLog("Can't open log file for #" + channel + " (" + nick + "): " + message); return; } QString logMessage {"["+nick+"] " + message + '\n'}; file.write(logMessage.toUtf8()); file.close(); } void IrcClient::consoleLog(const QString &message) { qInfo().noquote() << "[" + m_connectionData.displayName + "]" << message; } void IrcClient::errorLog(const QString &message) { QFile log(m_connectionData.logFolderPath + global::slash + "error.log"); consoleLog("[ERROR] " + message); if (log.open(QIODevice::WriteOnly | QIODevice::Append)) { log.write(QDateTime::currentDateTime().toString().toUtf8() + " " + message.toUtf8() + "\n"); log.close(); } } void IrcClient::onRead() { m_shouldAppendMessage ? m_readingBuffer += m_socket->readAll() : m_readingBuffer = m_socket->readAll(); if (not m_readingBuffer.endsWith('\n')) { if (not m_shouldAppendMessage) m_shouldAppendMessage = true; return; } if (m_shouldAppendMessage) m_shouldAppendMessage = false; m_readingBuffer.remove(QRegularExpression("\n$")); m_readingBuffer.remove('\r'); QStringList messageLines {m_readingBuffer.split('\n')}; for (auto &line: messageLines) { // consoleLog(line); process(line); } } void IrcClient::onLogin() { m_timerToJoin.stop(); if (not m_connectionData.password.isEmpty()) { write("PRIVMSG NICKSERV IDENTIFY " + m_connectionData.password); } if (m_connectionData.altNick.isEmpty()) { write("MODE " + m_connectionData.nick + " +B"); } else { write("MODE " + m_connectionData.altNick + " +B"); } for (auto &ch: m_connectionData.channels) { write("JOIN " + ch); } } void IrcClient::onDisconnected() { disconnect (m_socket, &QTcpSocket::readyRead, this, &IrcClient::onRead); disconnect (m_socket, &QTcpSocket::disconnected, this, &IrcClient::onDisconnected); m_socket->close(); m_socket->deleteLater(); m_connected = false; emit myOnline(m_connectionData.displayName, m_connected); m_pingTimeout.stop(); m_nickRecover.stop(); m_timerToJoin.stop(); m_usersActualize.stop(); consoleLog("Disconnected from server. Reconnecting..."); QThread::sleep(THREAD_SLEEP_ON_RECONNECT_TIMER); connectToServer(); } void IrcClient::pingTimedOut() { consoleLog("Ping timed out (361 seconds)"); if (m_socket != nullptr) { m_socket->disconnectFromHost(); } else { consoleLog("Socket already closed"); } } void IrcClient::actualizeUsersList() { consoleLog("Online list actualize..."); for (auto channel: m_connectionData.channels) { write("NAMES " + channel); } } void IrcClient::nickRecover() { write("NICK " + m_connectionData.nick); if (not m_connectionData.password.isEmpty()) { write("PRIVMSG NICKSERV IDENTIFY " + m_connectionData.password); } } void IrcClient::process(const QString &message) { if (m_readingBuffer.startsWith("PING")) { m_readingBuffer.remove("PING :"); write("PONG :" + m_readingBuffer, false); if (not m_connected) { consoleLog("Connected to server!"); m_timerToJoin.start(1000); m_usersActualize.start(USER_LIST_ACTIALIZE_TIMER); m_connected = true; emit myOnline(m_connectionData.displayName, m_connected); } m_pingTimeout.start(PING_TIMEOUT_TIMER); // 361 secs return; } IrcCode code {getServerCode(message)}; QString channel {getChannelName(message)}; QString nickname {getNickname(message)}; QString raw {message}; if (code != IrcCode::Failed) { if (code == IrcCode::NamesList) { raw.remove(QRegularExpression("^.*:")); if (m_readNamesList.contains(channel)) { m_online[channel] += raw.split(' '); } else { m_online[channel] = raw.split(' '); m_readNamesList.push_back(channel); } } else if (code == IrcCode::EndOfNamesList) { m_readNamesList.removeAll(channel); consoleLog("Online at " + channel + ": " + QString::number(m_online[channel].size()-1)); emit userOnline(m_connectionData.displayName, channel, m_online[channel]); } else if (code == IrcCode::ChannelTopic) { raw.remove(QRegularExpression("^.*\\s" + channel + "\\s:")); consoleLog("Topic at " + channel + ": " + raw); emit topicChanged(m_connectionData.displayName, channel, raw); } else if (code == IrcCode::NickNameIsAlreadyInUse) { if (m_connectionData.altNick.isEmpty()) { m_connectionData.altNick = m_connectionData.nick + "_" + global::getRandomString(9,3); write ("NICK " + m_connectionData.altNick); emit myNickname(m_connectionData.displayName, m_connectionData.altNick); m_nickRecover.start(NICK_RECOVER_TIMER); } } } else { // Если нет кода, есть строчное имя действия if (m_rgxPrivmsg.match(message).hasMatch()) { if (channel.isEmpty()) return; // Private message to bot QString userMsg {message}; userMsg.remove(QRegularExpression("^.*"+channel+"\\s:")); if (userMsg.startsWith('.')) { userMsg = global::BLINDED_MESSAGE_MERKER; } else if (userMsg.startsWith("ACTION")) { userMsg.remove("ACTION"); userMsg.remove(""); userMsg = "*** " + userMsg + " ***"; } consoleLog(channel + " (" + nickname + "): " + userMsg); QString myCurrentNick; if (m_connectionData.altNick.isEmpty()) { myCurrentNick = m_connectionData.nick; } else { myCurrentNick = m_connectionData.altNick; } if (QRegularExpression("^"+myCurrentNick+"(:|,|!).*").match(userMsg).hasMatch()) { userMsg.remove(0, myCurrentNick.size()); toTrigger(channel, nickname, userMsg); } else { toChatLog(channel, nickname, userMsg); } } else if (m_rgxJoin.match(message).hasMatch()) { if (nickname == m_connectionData.nick or nickname == m_connectionData.altNick) { consoleLog("I joined to " + channel); return; } m_online[channel].push_back(nickname); consoleLog("JOIN " + getNickname(message) + " to " + channel + ". " "Online: " + QString::number(m_online[channel].size()-1)); emit userOnline(m_connectionData.displayName, channel, m_online[channel]); } else if (m_rgxPart.match(message).hasMatch()) { const std::array specSymbols {'+', '&', '~', '@'}; for (const auto& sym: specSymbols) { m_online[channel].removeAll(sym+nickname); } m_online[channel].removeAll(nickname); consoleLog("PART " + getNickname(message) + " from " + channel + ". " "Online: " + QString::number(m_online[channel].size()-1)); emit userOnline(m_connectionData.displayName, channel, m_online[channel]); } else if (m_rgxKick.match(message).hasMatch()) { QString kickedUser {message}; kickedUser.remove(QRegularExpression("^.*"+channel+"\\s")); kickedUser.remove(QRegularExpression("\\s.*$")); m_online[channel].removeAll(kickedUser); consoleLog("KICK " + nickname + " from " + channel + ". " "Online: " + QString::number(m_online[channel].size()-1)); emit userOnline(m_connectionData.displayName, channel, m_online[channel]); } else if (m_rgxMode.match(message).hasMatch()) { if (QRegularExpression("^.*\\sMODE\\s"+channel+"\\s(\\+|-)b").match(message).hasMatch()) { write("NAMES "+channel); } } else if (m_rgxQuit.match(message).hasMatch()) { const std::array prefixes {'~' /*owner*/, '&' /*admin*/, '@' /*operator*/, '%' /*half-op*/, '+' /*voiced*/}; for (auto &ch: m_online) { ch.second.removeAll(nickname); for (const auto& p: prefixes) { ch.second.removeAll(p+nickname); } emit userOnline(m_connectionData.displayName, ch.first, ch.second); } consoleLog("QUIT " + nickname); } else if (m_rgxNick.match(message).hasMatch()) { if (message.contains(":" + m_connectionData.altNick + " NICK :" + m_connectionData.nick)) { // Успешная смена никнейма. Синтаксис ответа сервера ":старый_ник NICK :новый_ник" m_nickRecover.stop(); m_connectionData.altNick.clear(); emit myNickname(m_connectionData.displayName, m_connectionData.nick); consoleLog("Default nickname (" + m_connectionData.nick + ") is recovered!"); } else { QString oldNick {getNickname(message)}; QString newNick {message}; newNick.remove(QRegularExpression("^.*NICK :")); for (auto &ch: m_online) { for (auto &nick: ch.second) { if (nick == oldNick) nick = newNick; } } if (oldNick.isEmpty()) { consoleLog("I was renamed to " + newNick + " (!)"); if (newNick != m_connectionData.nick) { m_connectionData.altNick = newNick; emit myNickname(m_connectionData.displayName, m_connectionData.altNick); m_nickRecover.start(NICK_RECOVER_TIMER); } else { emit myNickname(m_connectionData.displayName, m_connectionData.nick); } } else { consoleLog("NICK " + oldNick + " renamed to " + newNick); emit userOnline(m_connectionData.displayName, channel, m_online[channel]); } } } else if (m_rgxTopic.match(message).hasMatch()) { raw.remove(QRegularExpression("^.*\\s" + channel + "\\s:")); consoleLog("Topic at " + channel + ": " + raw); emit topicChanged(m_connectionData.displayName, channel, raw); } } if (message.startsWith("ERROR")) { errorLog(message); } }