ircabot/src/ircnetworkclient.cpp

373 lines
12 KiB
C++

/*
This file is part of IRCaBot.
IRCaBot is IRC logger with features.
Source code: https://notabug.org/acetone/ircabot.
Copyright (C) acetone, 2023.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ircnetworkclient.h"
#include "currenttime.h"
#include "adminircpanel.h"
#include "voicegate.h"
#include "httpserver.h"
#include <QDateTime>
#include <QStringList>
#include <QRegularExpression>
#include <QDebug>
#define LOG_WARN qWarning().noquote() << "[" + m_config->networkName + "]"
#define LOG_INFO qInfo().noquote() << "[" + m_config->networkName + "]"
#define LOG_DEBUG qDebug().noquote() << "[" + m_config->networkName + "]"
constexpr const int VOICEGATE_REQUEST_MINIMAL_INTERVAL = 120;
constexpr const int VOICEGATE_REQUEST_WITHOUT_ANSWER_THRESHOLD = 30;
constexpr const char DIRECT_MESSAGE_TRIGGER_VOICEGATE[] = "voice";
IRCNetworkClient::IRCNetworkClient(QObject *parent) : QObject(parent) {}
void IRCNetworkClient::setConfig(Config::Network *&config)
{
if (config->instance != nullptr)
{
qFatal("IRCNetworkClient::setConfig receive config which initialized by another instanse");
}
config->instance = this;
m_config = config;
}
uint IRCNetworkClient::reloadChannelList()
{
uint changeCounter = 0;
QMutexLocker lock (&m_mtxNetworkChannelsIteration);
QStringList connectedChannelsTotalList;
// PART
QStringList channelsToPart;
QListIterator<libircclient::Channel*> connectedChannelListIterator(m_network->GetChannels());
while (connectedChannelListIterator.hasNext())
{
auto connectedChan = connectedChannelListIterator.next();
if (not m_config->channels.contains(connectedChan->GetName()))
{
channelsToPart.push_back(connectedChan->GetName());
changeCounter++;
}
connectedChannelsTotalList.push_back(connectedChan->GetName());
}
QStringListIterator channelsToPartIter (channelsToPart);
while (channelsToPartIter.hasNext())
{
const auto chanName = channelsToPartIter.next();
m_network->RequestPart(chanName);
QThread::msleep(500);
}
// JOIN
QStringList channelsToJoin;
QStringListIterator newChannelListIter (m_config->channels);
while (newChannelListIter.hasNext())
{
auto newChannel = newChannelListIter.next();
if (not connectedChannelsTotalList.contains(newChannel))
{
channelsToJoin.push_back(newChannel);
changeCounter++;
}
}
QStringListIterator channelsToJoinIter (channelsToJoin);
while (channelsToJoinIter.hasNext())
{
const auto chanName = channelsToJoinIter.next();
m_network->RequestJoin(chanName);
QThread::msleep(500);
}
return changeCounter;
}
void IRCNetworkClient::start()
{
if (m_network != nullptr)
{
LOG_WARN << "IRCNetworkClient::start() called, but already started";
return;
}
libirc::ServerAddress address(m_config->host);
address.SetPort(m_config->port);
address.SetSSL(m_config->ssl);
address.SetNick(m_config->nickname);
address.SetPassword(m_config->password);
address.SetIdent(m_config->ident);
address.SetRealname(m_config->realName);
address.SetSuffix(m_config->channels.join(','));
m_network = new libircclient::Network(address, m_config->networkName);
m_network->SetDefaultIdentifyString(m_config->identifyFormat);
m_network->Connect();
connect(m_network, &libircclient::Network::Event_WhoisRegNick, this, &IRCNetworkClient::Slot_adminIdentified);
connect(m_network, &libircclient::Network::Event_PRIVMSG, this, &IRCNetworkClient::Slot_privMsg);
connect(this, &IRCNetworkClient::Event_NewDirectMessage, this, &IRCNetworkClient::Slot_onDirectMessage);
connect(this, &IRCNetworkClient::Event_NewChannelMessage, this, &IRCNetworkClient::Slot_onChannelMessage);
connect(m_network, &libircclient::Network::Event_Connected, this, [&](){
LOG_INFO << "Connected to server";
});
connect(m_network, &libircclient::Network::Event_Disconnected, this, &IRCNetworkClient::Slot_onDisconnected);
connect(m_network, &libircclient::Network::Event_Join, this, &IRCNetworkClient::Slot_onJoin);
}
void IRCNetworkClient::Slot_privMsg(libircclient::Parser *p)
{
const QString receiver = p->GetParameterLine();
const QString sender = p->GetSourceUserInfo()->GetNick();
QString text = p->GetText();
static const QRegularExpression leadingSpaces("^\\s*");
static const QRegularExpression trailingSpaces("\\s*$");
text.remove(leadingSpaces);
text.remove(trailingSpaces);
if (text.isEmpty()) return;
if (receiver == m_network->GetLocalUserInfo()->GetNick())
{
emit Event_NewDirectMessage(sender, text);
}
else
{
emit Event_NewChannelMessage(receiver, sender, text);
}
}
void IRCNetworkClient::Slot_onDirectMessage(QString sender, QString text)
{
if (sender == m_config->ircabotAdmin and text.startsWith("!"))
{
processAdminCommand(text);
}
else if (text.startsWith(DIRECT_MESSAGE_TRIGGER_VOICEGATE, Qt::CaseInsensitive))
{
processVoiceRequest(sender);
}
// else
// {
// m_network->SendMessage("Known command: " + QString(DIRECT_MESSAGE_TRIGGER_VOICEGATE), sender, libircclient::Priority_Low);
// }
}
void IRCNetworkClient::Slot_onChannelMessage(QString channel, QString sender, QString text)
{
qCritical() << "Channel logging not implemented: " << channel << sender << text;
}
void IRCNetworkClient::Slot_onJoin(libircclient::Parser *, libircclient::User *u, libircclient::Channel *c)
{
if (u->GetNick() == m_network->GetLocalUserInfo()->GetNick())
{
LOG_INFO << "Joined to" << c->GetName();
return;
}
}
void IRCNetworkClient::Slot_onDisconnected()
{
LOG_INFO << "Disconnected from server. Auto reconnect" << (m_config->autoReconnect ? "enabled" : "disabled");
if (m_config->autoReconnect)
{
static std::atomic<int8_t> sleepPeriod = 1;
if (sleepPeriod > 20) sleepPeriod = 1;
QThread::sleep(sleepPeriod++);
m_network->Reconnect();
}
}
void IRCNetworkClient::processVoiceRequest(const QString& sender)
{
if (not m_config->voiceGate)
{
m_network->SendMessage("Voice gate disabled", sender);
return;
}
if (m_voiceGateRequestsTimestamp.find(sender) != m_voiceGateRequestsTimestamp.end())
{
const qint64 lastRequestSecsAgo = currentTimestampSecs() - m_voiceGateRequestsTimestamp[sender];
if (lastRequestSecsAgo < VOICEGATE_REQUEST_MINIMAL_INTERVAL)
{
if (lastRequestSecsAgo > VOICEGATE_REQUEST_WITHOUT_ANSWER_THRESHOLD)
{
m_network->SendMessage("Too many requests, try again later", sender, libircclient::Priority_Low);
}
qInfo().noquote() << "Request to voice rejected by minimal interval:" << VOICEGATE_REQUEST_MINIMAL_INTERVAL << "secs for" << sender;
return;
}
}
m_voiceGateRequestsTimestamp[sender] = currentTimestampSecs();
// TODO: check voiced cache
const QString invite = VoiceGate::getInviteToken( {m_config->networkName, sender} );
QString message = "Solve the captcha to get a voice in moderated channels where I have privileges: ";
QStringListIterator urlsIter(Config::webUiBaseUrls());
while (urlsIter.hasNext())
{
message += urlsIter.next() + QString(HTTP_URI_VOICEGATE_PREFIX) + "/" + QString(HTTP_URI_VOICEGATE_ACTION_INVITE) + "?token=" + invite;
if (urlsIter.hasNext()) message += " or ";
}
m_network->SendMessage(message, sender);
}
void IRCNetworkClient::Slot_adminIdentified(libircclient::Parser* p)
{
auto params = p->GetParameters();
if (params.size() < 2)
{
LOG_WARN << "Event_WhoisRegNick failure: params.size() < 2, expected 2";
return;
}
if (p->GetParameters().at(1) != m_config->ircabotAdmin)
{
LOG_WARN << "Event_WhoisRegNick failure: requested, but admin changed";
return;
}
AdminIRCPanel::identifiedTimestampUpdate(m_config->networkName);
processAdminCommand(m_adminCommandBuffer);
m_adminCommandBuffer.clear();
}
void IRCNetworkClient::processAdminCommand(const QString &text)
{
QStringList answer;
if (not AdminIRCPanel::parse(m_config->networkName, text, answer))
{
if (not m_adminCommandBuffer.isEmpty())
{
m_network->SendMessage("Your authorization is being verified. "
"Try repeating the request after a couple of seconds. "
"If your nickname is not registered, admin feature is not available.", m_config->ircabotAdmin);
}
m_adminCommandBuffer = text;
m_network->RequestWhois(m_config->ircabotAdmin);
return;
}
AdminIRCPanel::identifiedTimestampUpdate(m_config->networkName);
LOG_INFO << "Admin is doing something:" << text;
QStringListIterator answerIter (answer);
while (answerIter.hasNext())
{
m_network->SendMessage(answerIter.next(), m_config->ircabotAdmin, libircclient::Priority_RealTime);
if (answerIter.hasNext()) QThread::msleep(100);
}
}
QList<libircclient::Channel *> IRCNetworkClient::getModeratedChannelsWhereIAmHavePrivileges()
{
QList<libircclient::Channel*> result;
if (not m_config->voiceGate)
{
LOG_DEBUG << "no moderated channels, because voice gate disabled for this network";
return result;
}
QMutexLocker lock (&m_mtxNetworkChannelsIteration);
const auto allChannels = m_network->GetChannels();
QListIterator<libircclient::Channel*> allChanIter(allChannels);
while (allChanIter.hasNext())
{
auto channel = allChanIter.next();
auto iAmAtChannel = channel->GetUser(m_network->GetLocalUserInfo()->GetNick());
if (iAmAtChannel == nullptr)
{
LOG_WARN << "IRCNetworkClient::getModeratedChannelsWhereIAmHavePrivileges anomaly: can't get myself from" << channel->GetName();
continue;
}
auto highestCUMode = iAmAtChannel->GetHighestCUMode();
auto iHavePrivileges = highestCUMode != 0 and highestCUMode != 'v' /*simple voice flag, not a moder*/;
if (not iHavePrivileges)
{
continue;
}
if (channel->GetMode().GetIncluding().contains('m'))
{
result.push_back(channel);
}
}
return result;
}
void IRCNetworkClient::addNicknameToVoicedGroup(const QString &nickname)
{
giveTheVoiceFlag(QStringList() << nickname);
}
void IRCNetworkClient::giveTheVoiceFlag(const QStringList& nicknames)
{
const auto channels = getModeratedChannelsWhereIAmHavePrivileges();
// ^ also have this mutex, so should lock this after
if (channels.isEmpty())
{
m_network->SendNotice("So far, there is nowhere to use your voice flag", nicknames.join(','));
LOG_INFO << "Can't give voice to" << nicknames << "because I have not privileges or voice gate disabled";
return;
}
QMutexLocker lock (&m_mtxNetworkChannelsIteration);
QListIterator<libircclient::Channel*> chanIter(channels);
while (chanIter.hasNext())
{
auto channel = chanIter.next();
auto nicknamesAtChannel = channel->GetUsers();
QHashIterator<QString, libircclient::User*> nicksAtChanIter(nicknamesAtChannel);
while (nicksAtChanIter.hasNext())
{
auto someUserAtChannel = nicksAtChanIter.next();
if (nicknames.contains(someUserAtChannel.key(), Qt::CaseInsensitive))
{
if (someUserAtChannel.value()->GetHighestCUMode() != 0) continue; // already have voice or higher privs
m_network->TransferRaw("MODE " + channel->GetName() + " +v " + nicksAtChanIter.key());
LOG_DEBUG << "set +v for" << nicksAtChanIter.key() << "at" << channel->GetName();
QThread::msleep(100);
}
}
}
m_network->SendNotice("You are voiced", nicknames.join(','));
}