ircclient.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. #include "ircclient.h"
  2. #include "global.h"
  3. #include "version.h"
  4. #include <QCoreApplication>
  5. #include <QDateTime>
  6. #include <QDir>
  7. #include <QTimer>
  8. #include <QFile>
  9. const int NICK_RECOVER_TIMER = 60000; // 1 minute
  10. const int THREAD_SLEEP_ON_RECONNECT = 3; // seconds
  11. const int USER_LIST_ACTIALIZE = 1800000; // 30 minutes
  12. const int WRITING_STOP_TIMER = 1100; // 1.1 second
  13. const int PING_TIMEOUT = 361000; // 361 seconds
  14. IrcClient::IrcClient(const ConnectionData& config, QObject *parent) :
  15. QObject(parent),
  16. m_socket(nullptr),
  17. m_connectionData(config),
  18. m_reconnectReport(false),
  19. m_connected(false),
  20. m_triggersIsStopped(false),
  21. m_shouldAppendMessage(false)
  22. {
  23. if (m_connectionData.address.isEmpty()) {
  24. throw std::runtime_error("Empty ConnectionData::address! You trying wrong way to use it, man!");
  25. }
  26. QString path {global::toLowerAndNoSpaces(m_connectionData.displayName)};
  27. QDir dir(m_connectionData.logFolderPath);
  28. if (not dir.exists()) {
  29. dir.cdUp();
  30. dir.mkdir(path);
  31. }
  32. dir.cd(path);
  33. m_connectionData.channels.removeAll("");
  34. for (auto chan: m_connectionData.channels) {
  35. chan.remove('#');
  36. dir.mkdir(chan);
  37. }
  38. if (not QFile::exists(dir.path()+global::slash+"about_server.txt")) {
  39. QFile about(dir.path()+global::slash+"about_server.txt");
  40. if (about.open(QIODevice::WriteOnly)) {
  41. about.write("# Server description file.\n"
  42. "# HTML is supported. For line breaks, use <br>.\n\n"
  43. "<center>¯\\_(ツ)_/¯</center>\n");
  44. about.close();
  45. }
  46. else {
  47. errorLog("Creating "+dir.path()+global::slash+"about_server.txt is failed");
  48. }
  49. }
  50. connect (&m_pingTimeout, &QTimer::timeout, this, &IrcClient::pingTimedOut);
  51. connect (&m_usersActualize, &QTimer::timeout, this, &IrcClient::actualizeUsersList);
  52. connect (&m_nickRecover, &QTimer::timeout, this, &IrcClient::nickRecover);
  53. connect (&m_timerToJoin, &QTimer::timeout, this, &IrcClient::onLogin);
  54. connect (&m_stopWriting, &QTimer::timeout, this, [&](){m_triggersIsStopped = false;});
  55. m_stopWriting.setSingleShot(true);
  56. }
  57. IrcClient::~IrcClient()
  58. {
  59. m_socket->write("QUIT IRCaBot " + IRCABOT_VERSION.toUtf8() + ": Disconnected by admin");
  60. m_socket->disconnectFromHost();
  61. }
  62. QStringList IrcClient::getOnlineUsers(const QString &channel)
  63. {
  64. if (m_online.find(channel) == m_online.end()) {
  65. return QStringList();
  66. }
  67. return m_online[channel];
  68. }
  69. void IrcClient::connectToServer()
  70. {
  71. if (not m_reconnectReport) {
  72. emit myNickname(m_connectionData.displayName, m_connectionData.nick);
  73. emit myOnline(m_connectionData.displayName, m_connected);
  74. emit startInfo(m_connectionData.displayName, m_connectionData.channels);
  75. }
  76. m_socket = new QTcpSocket;
  77. m_socket->connectToHost(m_connectionData.address, m_connectionData.port);
  78. if (not m_socket->waitForConnected()) {
  79. if (not m_reconnectReport) {
  80. consoleLog("Connection failed. Reconnecting...");
  81. m_reconnectReport = true;
  82. }
  83. QThread::sleep(THREAD_SLEEP_ON_RECONNECT);
  84. connectToServer();
  85. return;
  86. }
  87. if (m_reconnectReport) {
  88. m_reconnectReport = false;
  89. }
  90. connect (m_socket, &QTcpSocket::disconnected, this, &IrcClient::onDisconnected);
  91. connect (m_socket, &QTcpSocket::readyRead, this, &IrcClient::onRead);
  92. write("USER " + m_connectionData.user + " . . " + m_connectionData.realName);
  93. write("NICK " + m_connectionData.nick);
  94. }
  95. void IrcClient::write(const QString &message, bool log)
  96. {
  97. if (m_socket == nullptr) {
  98. consoleLog("IrcClient::write() Socket is nullptr!");
  99. return;
  100. }
  101. if (log) {
  102. consoleLog("<- " + message);
  103. }
  104. m_socket->write(message.toUtf8() + '\n');
  105. if (not m_socket->waitForBytesWritten()) {
  106. consoleLog("IrcClient::write() Bytes was not written to socket!");
  107. return;
  108. }
  109. }
  110. IrcClient::IrcCode IrcClient::getServerCode(const QString &message)
  111. {
  112. IrcCode code = IrcCode::Failed;
  113. QString result {message};
  114. int beginPosition = result.indexOf(' ');
  115. if (beginPosition == -1) return code;
  116. result.remove(0, beginPosition+1);
  117. int endPosition = result.indexOf(' ');
  118. if (endPosition == -1) return code;
  119. result.remove(endPosition, result.size()-endPosition);
  120. bool convert {false};
  121. int resultInt {result.toInt(&convert)};
  122. if (not convert) return code;
  123. switch (resultInt) {
  124. case 332:
  125. code = IrcCode::ChannelTopic;
  126. break;
  127. case 353:
  128. code = IrcCode::NamesList;
  129. break;
  130. case 366:
  131. code = IrcCode::EndOfNamesList;
  132. break;
  133. case 433:
  134. code = IrcCode::NickNameIsAlreadyInUse;
  135. break;
  136. }
  137. return code;
  138. }
  139. QString IrcClient::getChannelName(const QString &message)
  140. {
  141. int begin = message.indexOf('#');
  142. if (begin == -1) return QString();
  143. QString result {message};
  144. result.remove(0,begin);
  145. int end = result.indexOf(' ');
  146. if (end == -1) {
  147. end = result.size();
  148. }
  149. else {
  150. result.remove(end, result.size()-end);
  151. }
  152. return result;
  153. }
  154. QString IrcClient::getNickname(const QString &message)
  155. {
  156. if (not message.startsWith(':')) return QString();
  157. QString result {message};
  158. result.remove(0,1);
  159. result.remove(QRegularExpression("!.*$"));
  160. if (result.contains(' ')) return QString();
  161. return result;
  162. }
  163. void IrcClient::toTrigger(const QString& channel, const QString &nickname, const QString &message)
  164. {
  165. if (m_connectionData.triggers.isEmpty()) return;
  166. if (m_triggersIsStopped) {
  167. consoleLog("IrcClient::toTrigger() Trigger request is ignored (anti DDoS)");
  168. return;
  169. }
  170. m_triggersIsStopped = true;
  171. m_stopWriting.start(WRITING_STOP_TIMER);
  172. for (auto trigger: m_connectionData.triggers) {
  173. if (message.contains(m_connectionData.triggers.key(trigger), Qt::CaseInsensitive)) {
  174. write("PRIVMSG " + channel + " " + nickname + ", " + trigger);
  175. return;
  176. }
  177. }
  178. QString possibleTriggers;
  179. for (auto trigger: m_connectionData.triggers) {
  180. possibleTriggers += "'" + m_connectionData.triggers.key(trigger) + "', ";
  181. }
  182. possibleTriggers.replace(QRegularExpression(",\\s$"), ".");
  183. write("PRIVMSG " + channel + " " + nickname + ": " + possibleTriggers);
  184. }
  185. void IrcClient::toChatLog(QString channel, const QString &nick, const QString &message)
  186. {
  187. channel.remove('#');
  188. emit newMessage(m_connectionData.displayName, channel, nick, message);
  189. QDir dir(m_connectionData.logFolderPath);
  190. if (not dir.exists(channel)) {
  191. dir.mkdir(channel);
  192. }
  193. if (not dir.cd(channel)) {
  194. errorLog("Can't open log folder (1) for #" + channel + " (" + nick + "): " + message);
  195. return;
  196. }
  197. QString year {QDateTime::currentDateTime().toString("yyyy")};
  198. if (not dir.cd(year)) {
  199. if (not dir.mkdir(year)) {
  200. errorLog("Can't create log folder (2) for #" + channel + " (" + nick + "): " + message);
  201. return;
  202. }
  203. if (not dir.cd(year)) {
  204. errorLog("Can't open log folder (2) for #" + channel + " (" + nick + "): " + message);
  205. return;
  206. }
  207. }
  208. QString month {QDateTime::currentDateTime().toString("MM")};
  209. if (not dir.cd(month)) {
  210. if (not dir.mkdir(month)) {
  211. errorLog("Can't create log folder (3) for #" + channel + " (" + nick + "): " + message);
  212. return;
  213. }
  214. if (not dir.cd(month)) {
  215. errorLog("Can't open log folder (3) for #" + channel + " (" + nick + "): " + message);
  216. return;
  217. }
  218. }
  219. QString day {QDateTime::currentDateTime().toString("dd")};
  220. QFile file (dir.path() + global::slash + day + ".txt");
  221. if (not file.open(QIODevice::WriteOnly | QIODevice::Append)) {
  222. errorLog("Can't open log file for #" + channel + " (" + nick + "): " + message);
  223. return;
  224. }
  225. QString logMessage {"["+nick+"] " + message + '\n'};
  226. file.write(logMessage.toUtf8());
  227. file.close();
  228. }
  229. void IrcClient::consoleLog(const QString &message)
  230. {
  231. qInfo().noquote() << "[" + m_connectionData.displayName + "]" << message;
  232. }
  233. void IrcClient::errorLog(const QString &message)
  234. {
  235. QFile log(m_connectionData.logFolderPath + global::slash + "error.log");
  236. consoleLog("[ERROR] " + message);
  237. if (log.open(QIODevice::WriteOnly | QIODevice::Append)) {
  238. log.write(QDateTime::currentDateTime().toString().toUtf8() + " " + message.toUtf8() + "\n");
  239. log.close();
  240. }
  241. }
  242. void IrcClient::onRead()
  243. {
  244. if (m_shouldAppendMessage) {
  245. m_readingBuffer += m_socket->readAll();
  246. } else {
  247. m_readingBuffer = m_socket->readAll();
  248. }
  249. if (not m_readingBuffer.endsWith('\n')) {
  250. if (not m_shouldAppendMessage) m_shouldAppendMessage = true;
  251. return;
  252. }
  253. if (m_shouldAppendMessage) m_shouldAppendMessage = false;
  254. if (m_readingBuffer.startsWith("PING")) {
  255. m_readingBuffer.remove("PING :");
  256. m_readingBuffer.remove("\r\n");
  257. write("PONG :" + m_readingBuffer, false);
  258. if (not m_connected) {
  259. consoleLog("Connected to server!");
  260. m_timerToJoin.start(1000);
  261. m_usersActualize.start(USER_LIST_ACTIALIZE);
  262. m_connected = true;
  263. emit myOnline(m_connectionData.displayName, m_connected);
  264. }
  265. m_pingTimeout.start(PING_TIMEOUT); // 361 secs
  266. return;
  267. }
  268. m_readingBuffer.remove(QRegularExpression("\\n$|\\r"));
  269. QStringList messageLines {m_readingBuffer.split('\n')};
  270. for (auto &line: messageLines) {
  271. //consoleLog(line);
  272. process(line);
  273. }
  274. }
  275. void IrcClient::onLogin()
  276. {
  277. m_timerToJoin.stop();
  278. if (not m_connectionData.password.isEmpty()) {
  279. write("PRIVMSG NICKSERV IDENTIFY " + m_connectionData.password);
  280. }
  281. if (m_connectionData.altNick.isEmpty()) {
  282. write("MODE " + m_connectionData.nick + " +B");
  283. }
  284. else {
  285. write("MODE " + m_connectionData.altNick + " +B");
  286. }
  287. for (auto &ch: m_connectionData.channels) {
  288. write("JOIN " + ch);
  289. }
  290. }
  291. void IrcClient::onDisconnected()
  292. {
  293. disconnect (m_socket, &QTcpSocket::readyRead, this, &IrcClient::onRead);
  294. disconnect (m_socket, &QTcpSocket::disconnected, this, &IrcClient::onDisconnected);
  295. m_socket->close();
  296. m_socket->deleteLater();
  297. m_connected = false;
  298. emit myOnline(m_connectionData.displayName, m_connected);
  299. m_pingTimeout.stop();
  300. m_nickRecover.stop();
  301. m_timerToJoin.stop();
  302. m_usersActualize.stop();
  303. consoleLog("Disconnected from server. Reconnecting...");
  304. QThread::sleep(THREAD_SLEEP_ON_RECONNECT);
  305. connectToServer();
  306. }
  307. void IrcClient::pingTimedOut()
  308. {
  309. consoleLog("Ping timed out (361 seconds)");
  310. if (m_socket != nullptr) {
  311. m_socket->disconnectFromHost();
  312. } else {
  313. consoleLog("Socket already closed");
  314. }
  315. }
  316. void IrcClient::actualizeUsersList()
  317. {
  318. consoleLog("Online list actualize...");
  319. for (auto channel: m_online) {
  320. write("NAMES " + channel.first);
  321. }
  322. }
  323. void IrcClient::nickRecover()
  324. {
  325. write("NICK " + m_connectionData.nick);
  326. if (not m_connectionData.password.isEmpty()) {
  327. write("PRIVMSG NICKSERV IDENTIFY " + m_connectionData.password);
  328. }
  329. }
  330. void IrcClient::process(const QString &message)
  331. {
  332. IrcCode code = getServerCode(message);
  333. QString channel = getChannelName(message);
  334. QString nickname {getNickname(message)};
  335. QString raw {message};
  336. if (code != IrcCode::Failed) {
  337. if (code == IrcCode::NamesList) {
  338. raw.remove(QRegularExpression("^.*:"));
  339. if (m_readNamesList.contains(channel)) {
  340. m_online[channel] += raw.split(' ');
  341. }
  342. else {
  343. m_online[channel] = raw.split(' ');
  344. m_readNamesList.push_back(channel);
  345. }
  346. }
  347. else if (code == IrcCode::EndOfNamesList) {
  348. m_readNamesList.removeAll(channel);
  349. consoleLog("Online at " + channel + ": " + QString::number(m_online[channel].size()-1));
  350. emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
  351. }
  352. else if (code == IrcCode::ChannelTopic) {
  353. raw.remove(QRegularExpression("^.*\\s" + channel + "\\s:"));
  354. consoleLog("Topic at " + channel + ": " + raw);
  355. emit topicChanged(m_connectionData.displayName, channel, raw);
  356. }
  357. else if (code == IrcCode::NickNameIsAlreadyInUse) {
  358. if (m_connectionData.altNick.isEmpty()) {
  359. m_connectionData.altNick = m_connectionData.nick + "_" + global::getRandomString(9,3);
  360. write ("NICK " + m_connectionData.altNick);
  361. emit myNickname(m_connectionData.displayName, m_connectionData.altNick);
  362. m_nickRecover.start(NICK_RECOVER_TIMER);
  363. }
  364. }
  365. }
  366. else {
  367. // Если нет кода, есть строчное имя действия
  368. if (m_rgxPrivmsg.match(message).hasMatch()) {
  369. if (channel.isEmpty()) return; // Private message to bot
  370. QString userMsg {message};
  371. userMsg.remove(QRegularExpression("^.*"+channel+"\\s:"));
  372. if (userMsg.startsWith('.')) {
  373. userMsg = global::BLINDED_MESSAGE_MERKER;
  374. }
  375. else if (userMsg.startsWith("ACTION")) {
  376. userMsg.remove("ACTION");
  377. userMsg.remove("");
  378. userMsg = "*** " + userMsg + " ***";
  379. }
  380. consoleLog(channel + " (" + nickname + "): " + userMsg);
  381. QString myCurrentNick;
  382. if (m_connectionData.altNick.isEmpty()) {
  383. myCurrentNick = m_connectionData.nick;
  384. }
  385. else {
  386. myCurrentNick = m_connectionData.altNick;
  387. }
  388. if (QRegularExpression("^"+myCurrentNick+"(:|,|!).*").match(userMsg).hasMatch()) {
  389. userMsg.remove(0, myCurrentNick.size());
  390. toTrigger(channel, nickname, userMsg);
  391. }
  392. else {
  393. toChatLog(channel, nickname, userMsg);
  394. }
  395. }
  396. else if (m_rgxJoin.match(message).hasMatch()) {
  397. if (nickname == m_connectionData.nick or nickname == m_connectionData.altNick) {
  398. consoleLog("I joined to " + channel);
  399. return;
  400. }
  401. m_online[channel].push_back(nickname);
  402. consoleLog("JOIN " + getNickname(message) + " to " + channel + ". "
  403. "Online: " + QString::number(m_online[channel].size()-1));
  404. emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
  405. }
  406. else if (m_rgxPart.match(message).hasMatch()) {
  407. const std::array<char,4> specSymbols {'+', '&', '~', '@'};
  408. for (const auto& sym: specSymbols) {
  409. m_online[channel].removeAll(sym+nickname);
  410. }
  411. m_online[channel].removeAll(nickname);
  412. consoleLog("PART " + getNickname(message) + " from " + channel + ". "
  413. "Online: " + QString::number(m_online[channel].size()-1));
  414. emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
  415. }
  416. else if (m_rgxKick.match(message).hasMatch()) {
  417. QString kickedUser {message};
  418. kickedUser.remove(QRegularExpression("^.*"+channel+"\\s"));
  419. kickedUser.remove(QRegularExpression("\\s.*$"));
  420. m_online[channel].removeAll(kickedUser);
  421. consoleLog("KICK " + nickname + " from " + channel + ". "
  422. "Online: " + QString::number(m_online[channel].size()-1));
  423. emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
  424. }
  425. else if (m_rgxMode.match(message).hasMatch()) {
  426. if (QRegularExpression("^.*\\sMODE\\s"+channel+"\\s(\\+|-)b").match(message).hasMatch()) {
  427. write("NAMES "+channel);
  428. }
  429. }
  430. else if (m_rgxQuit.match(message).hasMatch()) {
  431. const std::array<char, 5> prefixes {'~' /*owner*/, '&' /*admin*/,
  432. '@' /*operator*/, '%' /*half-op*/,
  433. '+' /*voiced*/};
  434. for (auto &ch: m_online) {
  435. ch.second.removeAll(nickname);
  436. for (const auto& p: prefixes) {
  437. ch.second.removeAll(p+nickname);
  438. }
  439. emit userOnline(m_connectionData.displayName, ch.first, ch.second);
  440. }
  441. consoleLog("QUIT " + nickname);
  442. }
  443. else if (m_rgxNick.match(message).hasMatch()) {
  444. if (message.contains(":" + m_connectionData.altNick + " NICK :" + m_connectionData.nick)) {
  445. // Успешная смена никнейма. Синтаксис ответа сервера ":старый_ник NICK :новый_ник"
  446. m_nickRecover.stop();
  447. m_connectionData.altNick.clear();
  448. emit myNickname(m_connectionData.displayName, m_connectionData.nick);
  449. consoleLog("Default nickname (" + m_connectionData.nick + ") is recovered!");
  450. }
  451. else {
  452. QString oldNick {getNickname(message)};
  453. QString newNick {message};
  454. newNick.remove(QRegularExpression("^.*NICK :"));
  455. for (auto &ch: m_online) {
  456. for (auto &nick: ch.second) {
  457. if (nick == oldNick) nick = newNick;
  458. }
  459. }
  460. if (oldNick.isEmpty()) {
  461. consoleLog("I was renamed to " + newNick + " (!)");
  462. if (newNick != m_connectionData.nick) {
  463. m_connectionData.altNick = newNick;
  464. emit myNickname(m_connectionData.displayName, m_connectionData.altNick);
  465. m_nickRecover.start(NICK_RECOVER_TIMER);
  466. }
  467. else {
  468. emit myNickname(m_connectionData.displayName, m_connectionData.nick);
  469. }
  470. }
  471. else {
  472. consoleLog("NICK " + oldNick + " renamed to " + newNick);
  473. emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
  474. }
  475. }
  476. }
  477. else if (m_rgxTopic.match(message).hasMatch()) {
  478. raw.remove(QRegularExpression("^.*\\s" + channel + "\\s:"));
  479. consoleLog("Topic at " + channel + ": " + raw);
  480. emit topicChanged(m_connectionData.displayName, channel, raw);
  481. }
  482. }
  483. if (message.startsWith("ERROR")) {
  484. errorLog(message);
  485. }
  486. }