mirror of https://notabug.org/acetone/ircabot.git
546 lines
19 KiB
C++
546 lines
19 KiB
C++
#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);
|
||
}
|
||
}
|