mirror of https://notabug.org/acetone/ircabot.git
373 lines
12 KiB
C++
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(','));
|
|
}
|