#include "ircclient.h" #include "global.h" #include #include #include #include #include const int NICK_RECOVER_TIMER = 60000; // 1 minute IrcClient::IrcClient(const ConnectionData& config, QObject *parent) : QObject(parent), m_socket(nullptr), m_connectionData(config), m_reconnectReport(false), m_connected(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); } 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); } IrcClient::~IrcClient() { 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; } 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 353: code = IrcCode::NamesList; 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; for (auto trigger: m_connectionData.triggers) { if (message.contains(m_connectionData.triggers.key(trigger), Qt::CaseInsensitive)) { write("PRIVMSG " + channel + " " + nickname + ", " + trigger); return; } } QString possibleTriggers; for (auto trigger: m_connectionData.triggers) { possibleTriggers += "'" + m_connectionData.triggers.key(trigger) + "', "; } possibleTriggers.replace(QRegularExpression(",\\s$"), "."); write("PRIVMSG " + channel + " " + nickname + ": " + possibleTriggers); } void IrcClient::toChatLog(QString channel, const QString &nick, const QString &message) { channel.remove('#'); 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() { if (m_shouldAppendMessage) { m_readingBuffer += m_socket->readAll(); } else { 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; if (m_readingBuffer.startsWith("PING")) { m_readingBuffer.remove("PING :"); m_readingBuffer.remove("\r\n"); write("PONG :" + m_readingBuffer, false); if (not m_connected) { consoleLog("Connected to server!"); m_timerToJoin.start(1000); m_usersActualize.start(1800000); // 30 munites m_connected = true; emit myOnline(m_connectionData.displayName, m_connected); } m_pingTimeout.start(361000); // 361 secs return; } m_readingBuffer.remove(QRegularExpression("\\n$|\\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..."); 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_online) { write("NAMES " + channel.first); } } 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) { 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("^.*:")); m_online[channel] = raw.split(' '); consoleLog("Online at " + channel + ": " + QString::number(m_online[channel].size()-1)); emit someAction(m_connectionData.displayName, channel, m_online[channel]); } 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 = "Blinded message"; } 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 (userMsg.startsWith(myCurrentNick)) { 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 someAction(m_connectionData.displayName, channel, m_online[channel]); } else if (m_rgxPart.match(message).hasMatch()) { m_online[channel].removeAll(nickname); consoleLog("PART " + getNickname(message) + " from " + channel + ". " "Online: " + QString::number(m_online[channel].size()-1)); emit someAction(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 someAction(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()) { for (auto &ch: m_online) { ch.second.removeAll(nickname); emit someAction(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_connectionData.altNick.clear(); m_nickRecover.stop(); emit myNickname(m_connectionData.displayName, m_connectionData.nick); } 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 someAction(m_connectionData.displayName, channel, m_online[channel]); } } } } if (message.startsWith("ERROR")) { QFile log(m_connectionData.logFolderPath + global::slash + "error.log"); errorLog(message); } }