ircclient.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  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. connect (&m_pingTimeout, &QTimer::timeout, this, &IrcClient::pingTimedOut);
  39. connect (&m_usersActualize, &QTimer::timeout, this, &IrcClient::actualizeUsersList);
  40. connect (&m_nickRecover, &QTimer::timeout, this, &IrcClient::nickRecover);
  41. connect (&m_timerToJoin, &QTimer::timeout, this, &IrcClient::onLogin);
  42. connect (&m_stopWriting, &QTimer::timeout, this, [&](){m_triggersIsStopped = false;});
  43. m_stopWriting.setSingleShot(true);
  44. }
  45. IrcClient::~IrcClient()
  46. {
  47. m_socket->write("QUIT IRCaBot " + IRCABOT_VERSION.toUtf8() + ": Disconnected by admin");
  48. m_socket->disconnectFromHost();
  49. }
  50. QStringList IrcClient::getOnlineUsers(const QString &channel)
  51. {
  52. if (m_online.find(channel) == m_online.end()) {
  53. return QStringList();
  54. }
  55. return m_online[channel];
  56. }
  57. void IrcClient::connectToServer()
  58. {
  59. if (not m_reconnectReport) {
  60. emit myNickname(m_connectionData.displayName, m_connectionData.nick);
  61. emit myOnline(m_connectionData.displayName, m_connected);
  62. emit startInfo(m_connectionData.displayName, m_connectionData.channels);
  63. }
  64. m_socket = new QTcpSocket;
  65. m_socket->connectToHost(m_connectionData.address, m_connectionData.port);
  66. if (not m_socket->waitForConnected()) {
  67. if (not m_reconnectReport) {
  68. consoleLog("Connection failed. Reconnecting...");
  69. m_reconnectReport = true;
  70. }
  71. QThread::sleep(THREAD_SLEEP_ON_RECONNECT);
  72. connectToServer();
  73. return;
  74. }
  75. if (m_reconnectReport) {
  76. m_reconnectReport = false;
  77. }
  78. connect (m_socket, &QTcpSocket::disconnected, this, &IrcClient::onDisconnected);
  79. connect (m_socket, &QTcpSocket::readyRead, this, &IrcClient::onRead);
  80. write("USER " + m_connectionData.user + " . . " + m_connectionData.realName);
  81. write("NICK " + m_connectionData.nick);
  82. }
  83. void IrcClient::write(const QString &message, bool log)
  84. {
  85. if (m_socket == nullptr) {
  86. consoleLog("IrcClient::write() Socket is nullptr!");
  87. return;
  88. }
  89. if (log) {
  90. consoleLog("<- " + message);
  91. }
  92. m_socket->write(message.toUtf8() + '\n');
  93. if (not m_socket->waitForBytesWritten()) {
  94. consoleLog("IrcClient::write() Bytes was not written to socket!");
  95. return;
  96. }
  97. }
  98. IrcClient::IrcCode IrcClient::getServerCode(const QString &message)
  99. {
  100. IrcCode code = IrcCode::Failed;
  101. QString result {message};
  102. int beginPosition = result.indexOf(' ');
  103. if (beginPosition == -1) return code;
  104. result.remove(0, beginPosition+1);
  105. int endPosition = result.indexOf(' ');
  106. if (endPosition == -1) return code;
  107. result.remove(endPosition, result.size()-endPosition);
  108. bool convert {false};
  109. int resultInt {result.toInt(&convert)};
  110. if (not convert) return code;
  111. switch (resultInt) {
  112. case 332:
  113. code = IrcCode::ChannelTopic;
  114. break;
  115. case 353:
  116. code = IrcCode::NamesList;
  117. break;
  118. case 366:
  119. code = IrcCode::EndOfNamesList;
  120. break;
  121. case 433:
  122. code = IrcCode::NickNameIsAlreadyInUse;
  123. break;
  124. }
  125. return code;
  126. }
  127. QString IrcClient::getChannelName(const QString &message)
  128. {
  129. int begin = message.indexOf('#');
  130. if (begin == -1) return QString();
  131. QString result {message};
  132. result.remove(0,begin);
  133. int end = result.indexOf(' ');
  134. if (end == -1) {
  135. end = result.size();
  136. }
  137. else {
  138. result.remove(end, result.size()-end);
  139. }
  140. return result;
  141. }
  142. QString IrcClient::getNickname(const QString &message)
  143. {
  144. if (not message.startsWith(':')) return QString();
  145. QString result {message};
  146. result.remove(0,1);
  147. result.remove(QRegularExpression("!.*$"));
  148. if (result.contains(' ')) return QString();
  149. return result;
  150. }
  151. void IrcClient::toTrigger(const QString& channel, const QString &nickname, const QString &message)
  152. {
  153. if (m_connectionData.triggers.isEmpty()) return;
  154. if (m_triggersIsStopped) {
  155. consoleLog("IrcClient::toTrigger() Trigger request is ignored (anti DDoS)");
  156. return;
  157. }
  158. m_triggersIsStopped = true;
  159. m_stopWriting.start(WRITING_STOP_TIMER);
  160. for (auto trigger: m_connectionData.triggers) {
  161. if (message.contains(m_connectionData.triggers.key(trigger), Qt::CaseInsensitive)) {
  162. write("PRIVMSG " + channel + " " + nickname + ", " + trigger);
  163. return;
  164. }
  165. }
  166. QString possibleTriggers;
  167. for (auto trigger: m_connectionData.triggers) {
  168. possibleTriggers += "'" + m_connectionData.triggers.key(trigger) + "', ";
  169. }
  170. possibleTriggers.replace(QRegularExpression(",\\s$"), ".");
  171. write("PRIVMSG " + channel + " " + nickname + ": " + possibleTriggers);
  172. }
  173. void IrcClient::toChatLog(QString channel, const QString &nick, const QString &message)
  174. {
  175. channel.remove('#');
  176. QDir dir(m_connectionData.logFolderPath);
  177. if (not dir.exists(channel)) {
  178. dir.mkdir(channel);
  179. }
  180. if (not dir.cd(channel)) {
  181. errorLog("Can't open log folder (1) for #" + channel + " (" + nick + "): " + message);
  182. return;
  183. }
  184. QString year {QDateTime::currentDateTime().toString("yyyy")};
  185. if (not dir.cd(year)) {
  186. if (not dir.mkdir(year)) {
  187. errorLog("Can't create log folder (2) for #" + channel + " (" + nick + "): " + message);
  188. return;
  189. }
  190. if (not dir.cd(year)) {
  191. errorLog("Can't open log folder (2) for #" + channel + " (" + nick + "): " + message);
  192. return;
  193. }
  194. }
  195. QString month {QDateTime::currentDateTime().toString("MM")};
  196. if (not dir.cd(month)) {
  197. if (not dir.mkdir(month)) {
  198. errorLog("Can't create log folder (3) for #" + channel + " (" + nick + "): " + message);
  199. return;
  200. }
  201. if (not dir.cd(month)) {
  202. errorLog("Can't open log folder (3) for #" + channel + " (" + nick + "): " + message);
  203. return;
  204. }
  205. }
  206. QString day {QDateTime::currentDateTime().toString("dd")};
  207. QFile file (dir.path() + global::slash + day + ".txt");
  208. if (not file.open(QIODevice::WriteOnly | QIODevice::Append)) {
  209. errorLog("Can't open log file for #" + channel + " (" + nick + "): " + message);
  210. return;
  211. }
  212. QString logMessage {"["+nick+"] " + message + '\n'};
  213. file.write(logMessage.toUtf8());
  214. file.close();
  215. }
  216. void IrcClient::consoleLog(const QString &message)
  217. {
  218. qInfo().noquote() << "[" + m_connectionData.displayName + "]" << message;
  219. }
  220. void IrcClient::errorLog(const QString &message)
  221. {
  222. QFile log(m_connectionData.logFolderPath + global::slash + "error.log");
  223. consoleLog("[ERROR] " + message);
  224. if (log.open(QIODevice::WriteOnly | QIODevice::Append)) {
  225. log.write(QDateTime::currentDateTime().toString().toUtf8() + " " + message.toUtf8() + "\n");
  226. log.close();
  227. }
  228. }
  229. void IrcClient::onRead()
  230. {
  231. if (m_shouldAppendMessage) {
  232. m_readingBuffer += m_socket->readAll();
  233. } else {
  234. m_readingBuffer = m_socket->readAll();
  235. }
  236. if (not m_readingBuffer.endsWith('\n')) {
  237. if (not m_shouldAppendMessage) m_shouldAppendMessage = true;
  238. return;
  239. }
  240. if (m_shouldAppendMessage) m_shouldAppendMessage = false;
  241. if (m_readingBuffer.startsWith("PING")) {
  242. m_readingBuffer.remove("PING :");
  243. m_readingBuffer.remove("\r\n");
  244. write("PONG :" + m_readingBuffer, false);
  245. if (not m_connected) {
  246. consoleLog("Connected to server!");
  247. m_timerToJoin.start(1000);
  248. m_usersActualize.start(USER_LIST_ACTIALIZE);
  249. m_connected = true;
  250. emit myOnline(m_connectionData.displayName, m_connected);
  251. }
  252. m_pingTimeout.start(PING_TIMEOUT); // 361 secs
  253. return;
  254. }
  255. m_readingBuffer.remove(QRegularExpression("\\n$|\\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);
  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_online) {
  307. write("NAMES " + channel.first);
  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. IrcCode code = getServerCode(message);
  320. QString channel = getChannelName(message);
  321. QString nickname {getNickname(message)};
  322. QString raw {message};
  323. if (code != IrcCode::Failed) {
  324. if (code == IrcCode::NamesList) {
  325. raw.remove(QRegularExpression("^.*:"));
  326. if (m_readNamesList.contains(channel)) {
  327. m_online[channel] += raw.split(' ');
  328. }
  329. else {
  330. m_online[channel] = raw.split(' ');
  331. m_readNamesList.push_back(channel);
  332. }
  333. }
  334. else if (code == IrcCode::EndOfNamesList) {
  335. m_readNamesList.removeAll(channel);
  336. consoleLog("Online at " + channel + ": " + QString::number(m_online[channel].size()-1));
  337. emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
  338. }
  339. else if (code == IrcCode::ChannelTopic) {
  340. raw.remove(QRegularExpression("^.*\\s" + channel + "\\s:"));
  341. consoleLog("Topic at " + channel + ": " + raw);
  342. emit topicChanged(m_connectionData.displayName, channel, raw);
  343. }
  344. else if (code == IrcCode::NickNameIsAlreadyInUse) {
  345. if (m_connectionData.altNick.isEmpty()) {
  346. m_connectionData.altNick = m_connectionData.nick + "_" + global::getRandomString(9,3);
  347. write ("NICK " + m_connectionData.altNick);
  348. emit myNickname(m_connectionData.displayName, m_connectionData.altNick);
  349. m_nickRecover.start(NICK_RECOVER_TIMER);
  350. }
  351. }
  352. }
  353. else {
  354. // Если нет кода, есть строчное имя действия
  355. if (m_rgxPrivmsg.match(message).hasMatch()) {
  356. if (channel.isEmpty()) return; // Private message to bot
  357. QString userMsg {message};
  358. userMsg.remove(QRegularExpression("^.*"+channel+"\\s:"));
  359. if (userMsg.startsWith('.')) {
  360. userMsg = "Blinded message";
  361. }
  362. else if (userMsg.startsWith("ACTION")) {
  363. userMsg.remove("ACTION");
  364. userMsg.remove("");
  365. userMsg = "*** " + userMsg + " ***";
  366. }
  367. consoleLog(channel + " (" + nickname + "): " + userMsg);
  368. QString myCurrentNick;
  369. if (m_connectionData.altNick.isEmpty()) {
  370. myCurrentNick = m_connectionData.nick;
  371. }
  372. else {
  373. myCurrentNick = m_connectionData.altNick;
  374. }
  375. if (QRegularExpression("^"+myCurrentNick+"(:|,|!).*").match(userMsg).hasMatch()) {
  376. userMsg.remove(0, myCurrentNick.size());
  377. toTrigger(channel, nickname, userMsg);
  378. }
  379. else {
  380. toChatLog(channel, nickname, userMsg);
  381. }
  382. }
  383. else if (m_rgxJoin.match(message).hasMatch()) {
  384. if (nickname == m_connectionData.nick or nickname == m_connectionData.altNick) {
  385. consoleLog("I joined to " + channel);
  386. return;
  387. }
  388. m_online[channel].push_back(nickname);
  389. consoleLog("JOIN " + getNickname(message) + " to " + channel + ". "
  390. "Online: " + QString::number(m_online[channel].size()-1));
  391. emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
  392. }
  393. else if (m_rgxPart.match(message).hasMatch()) {
  394. m_online[channel].removeAll(nickname);
  395. consoleLog("PART " + getNickname(message) + " from " + channel + ". "
  396. "Online: " + QString::number(m_online[channel].size()-1));
  397. emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
  398. }
  399. else if (m_rgxKick.match(message).hasMatch()) {
  400. QString kickedUser {message};
  401. kickedUser.remove(QRegularExpression("^.*"+channel+"\\s"));
  402. kickedUser.remove(QRegularExpression("\\s.*$"));
  403. m_online[channel].removeAll(kickedUser);
  404. consoleLog("KICK " + nickname + " from " + 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_rgxMode.match(message).hasMatch()) {
  409. if (QRegularExpression("^.*\\sMODE\\s"+channel+"\\s(\\+|-)b").match(message).hasMatch()) {
  410. write("NAMES "+channel);
  411. }
  412. }
  413. else if (m_rgxQuit.match(message).hasMatch()) {
  414. const std::array<char, 5> prefixes {'~' /*owner*/, '&' /*admin*/,
  415. '@' /*operator*/, '%' /*half-op*/,
  416. '+' /*voiced*/};
  417. for (auto &ch: m_online) {
  418. ch.second.removeAll(nickname);
  419. for (const auto& p: prefixes) {
  420. ch.second.removeAll(p+nickname);
  421. }
  422. emit userOnline(m_connectionData.displayName, ch.first, ch.second);
  423. }
  424. consoleLog("QUIT " + nickname);
  425. }
  426. else if (m_rgxNick.match(message).hasMatch()) {
  427. if (message.contains(":" + m_connectionData.altNick + " NICK :" + m_connectionData.nick)) {
  428. // Успешная смена никнейма. Синтаксис ответа сервера ":старый_ник NICK :новый_ник"
  429. m_nickRecover.stop();
  430. m_connectionData.altNick.clear();
  431. emit myNickname(m_connectionData.displayName, m_connectionData.nick);
  432. consoleLog("Default nickname (" + m_connectionData.nick + ") is recovered!");
  433. }
  434. else {
  435. QString oldNick {getNickname(message)};
  436. QString newNick {message};
  437. newNick.remove(QRegularExpression("^.*NICK :"));
  438. for (auto &ch: m_online) {
  439. for (auto &nick: ch.second) {
  440. if (nick == oldNick) nick = newNick;
  441. }
  442. }
  443. if (oldNick.isEmpty()) {
  444. consoleLog("I was renamed to " + newNick + " (!)");
  445. if (newNick != m_connectionData.nick) {
  446. m_connectionData.altNick = newNick;
  447. emit myNickname(m_connectionData.displayName, m_connectionData.altNick);
  448. m_nickRecover.start(NICK_RECOVER_TIMER);
  449. }
  450. else {
  451. emit myNickname(m_connectionData.displayName, m_connectionData.nick);
  452. }
  453. }
  454. else {
  455. consoleLog("NICK " + oldNick + " renamed to " + newNick);
  456. emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
  457. }
  458. }
  459. }
  460. else if (m_rgxTopic.match(message).hasMatch()) {
  461. raw.remove(QRegularExpression("^.*\\s" + channel + "\\s:"));
  462. consoleLog("Topic at " + channel + ": " + raw);
  463. emit topicChanged(m_connectionData.displayName, channel, raw);
  464. }
  465. }
  466. if (message.startsWith("ERROR")) {
  467. errorLog(message);
  468. }
  469. }