ircabot/ircclient.cpp

478 lines
16 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>
const int NICK_RECOVER_TIMER = 60000; // 1 minute
IrcClient::IrcClient(const ConnectionData& config, QObject *parent) :
QObject(parent),
m_socket(nullptr),
m_connectionData(config),
m_reconnectReport(false),
m_connected(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);
}
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);
}
IrcClient::~IrcClient()
{
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;
}
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 353:
code = IrcCode::NamesList;
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;
for (auto trigger: m_connectionData.triggers) {
if (message.contains(m_connectionData.triggers.key(trigger), Qt::CaseInsensitive)) {
write("PRIVMSG " + channel + " " + nickname + ", " + trigger);
return;
}
}
QString possibleTriggers;
for (auto trigger: m_connectionData.triggers) {
possibleTriggers += "'" + m_connectionData.triggers.key(trigger) + "', ";
}
possibleTriggers.replace(QRegularExpression(",\\s$"), ".");
write("PRIVMSG " + channel + " " + nickname + ": " + possibleTriggers);
}
void IrcClient::toChatLog(QString channel, const QString &nick, const QString &message)
{
channel.remove('#');
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()
{
if (m_shouldAppendMessage) {
m_readingBuffer += m_socket->readAll();
} else {
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;
if (m_readingBuffer.startsWith("PING")) {
m_readingBuffer.remove("PING :");
m_readingBuffer.remove("\r\n");
write("PONG :" + m_readingBuffer, false);
if (not m_connected) {
consoleLog("Connected to server!");
m_timerToJoin.start(1000);
m_usersActualize.start(1800000); // 30 munites
m_connected = true;
emit myOnline(m_connectionData.displayName, m_connected);
}
m_pingTimeout.start(361000); // 361 secs
return;
}
m_readingBuffer.remove(QRegularExpression("\\n$|\\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...");
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_online) {
write("NAMES " + channel.first);
}
}
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)
{
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("^.*:"));
m_online[channel] = raw.split(' ');
consoleLog("Online at " + channel + ": " + QString::number(m_online[channel].size()-1));
emit someAction(m_connectionData.displayName, channel, m_online[channel]);
}
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 = "Blinded message";
}
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 (userMsg.startsWith(myCurrentNick)) {
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 someAction(m_connectionData.displayName, channel, m_online[channel]);
}
else if (m_rgxPart.match(message).hasMatch()) {
m_online[channel].removeAll(nickname);
consoleLog("PART " + getNickname(message) + " from " + channel + ". "
"Online: " + QString::number(m_online[channel].size()-1));
emit someAction(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 someAction(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()) {
for (auto &ch: m_online) {
ch.second.removeAll(nickname);
emit someAction(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_connectionData.altNick.clear();
m_nickRecover.stop();
emit myNickname(m_connectionData.displayName, m_connectionData.nick);
}
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 someAction(m_connectionData.displayName, channel, m_online[channel]);
}
}
}
}
if (message.startsWith("ERROR")) {
QFile log(m_connectionData.logFolderPath + global::slash + "error.log");
errorLog(message);
}
}