ircabot/ircclient.cpp

546 lines
19 KiB
C++
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#include "ircclient.h"
#include "global.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QTimer>
#include <QFile>
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 " + global::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_TIMER);
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)) {
QString response {trigger};
response.replace(TRIGGER_CHANNEL_FOR_URL_PATTERN,
global::toLowerAndNoSpaces(m_connectionData.displayName) + "/" + channel.mid(1, channel.size()-1));
write("PRIVMSG " + channel + " " + nickname + ", " + response);
return;
}
}
QString possibleTriggers;
for (auto trigger: m_connectionData.triggers) {
possibleTriggers += "'" + m_connectionData.triggers.key(trigger) + "', ";
}
possibleTriggers.replace(QRegularExpression(",\\s$"), ".");
write("PRIVMSG " + channel + " " + nickname + ", try it: " + possibleTriggers);
}
void IrcClient::toChatLog(QString channel, const QString &nick, const QString &message)
{
QString msg {message}; // Empty message ignored
msg.remove(" ");
msg.remove("\t");
if (msg.isEmpty()) return;
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()
{
m_shouldAppendMessage ?
m_readingBuffer += m_socket->readAll() :
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;
m_readingBuffer.remove(QRegularExpression("\n$"));
m_readingBuffer.remove('\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_TIMER);
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_connectionData.channels) {
write("NAMES " + channel);
}
}
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)
{
if (m_readingBuffer.startsWith("PING"))
{
m_readingBuffer.remove("PING :");
write("PONG :" + m_readingBuffer, false);
if (not m_connected)
{
consoleLog("Connected to server!");
m_timerToJoin.start(1000);
m_usersActualize.start(USER_LIST_ACTIALIZE_TIMER);
m_connected = true;
emit myOnline(m_connectionData.displayName, m_connected);
}
m_pingTimeout.start(PING_TIMEOUT_TIMER); // 361 secs
return;
}
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);
}
}