/* 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 . */ #include "ircnetworkclient.h" #include "currenttime.h" #include "adminircpanel.h" #include "voicegate.h" #include "httpserver.h" #include #include #include #include #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 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 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 IRCNetworkClient::getModeratedChannelsWhereIAmHavePrivileges() { QList 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 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 chanIter(channels); while (chanIter.hasNext()) { auto channel = chanIter.next(); auto nicknamesAtChannel = channel->GetUsers(); QHashIterator 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(',')); }