123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543 |
- #include "ircclient.h"
- #include "global.h"
- #include "version.h"
- #include <QCoreApplication>
- #include <QDateTime>
- #include <QDir>
- #include <QTimer>
- #include <QFile>
- const int NICK_RECOVER_TIMER = 60000; // 1 minute
- const int THREAD_SLEEP_ON_RECONNECT = 3; // seconds
- const int USER_LIST_ACTIALIZE = 1800000; // 30 minutes
- const int WRITING_STOP_TIMER = 1100; // 1.1 second
- const int PING_TIMEOUT = 361000; // 361 seconds
- 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 <br>.\n\n"
- "<center>¯\\_(ツ)_/¯</center>\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 " + 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);
- 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)) {
- 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('#');
- 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()
- {
- 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(USER_LIST_ACTIALIZE);
- m_connected = true;
- emit myOnline(m_connectionData.displayName, m_connected);
- }
- m_pingTimeout.start(PING_TIMEOUT); // 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...");
- QThread::sleep(THREAD_SLEEP_ON_RECONNECT);
- 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("^.*:"));
- 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<char,4> 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<char, 5> 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);
- }
- }
|