ircclient.cpp 19 KB

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