// Anope IRC Services // // Copyright (C) 2003-2026 Anope Contributors // // Anope is free software. You can use, modify, and/or distribute it under the // terms of version 2 of the GNU General Public License. See docs/LICENSE.txt // for the complete terms of this license and docs/AUTHORS.txt for a list of // contributors. // // Based on the original code of Epona by Lara // Based on the original code of Services by Andy Church // // SPDX-License-Identifier: GPL-2.0-only #include "module.h" #include "modules/botserv/kick.h" #include "modules/botserv/badwords.h" static Module *me; struct KickerDataImpl final : BotServ::KickerData { KickerDataImpl(Extensible *obj) { amsgs = badwords = bolds = caps = colors = flood = italics = repeat = reverses = underlines = false; for (auto &ttbtype : ttb) ttbtype = 0; capsmin = capspercent = 0; floodlines = floodsecs = 0; repeattimes = 0; dontkickops = dontkickvoices = false; } void Check(ChannelInfo *ci) override { if (amsgs || badwords || bolds || caps || colors || flood || italics || repeat || reverses || underlines || dontkickops || dontkickvoices) return; ci->Shrink(BOTSERV_KICKER_DATA_EXT); } struct ExtensibleItem final : ::ExtensibleItem { ExtensibleItem(Module *m, const Anope::string &ename) : ::ExtensibleItem(m, ename) { } void ExtensibleSerialize(const Extensible *e, const Serializable *s, Serialize::Data &data) const override { if (s->GetSerializableType()->GetName() != CHANNELINFO_TYPE) return; const auto *ci = anope_dynamic_static_cast(e); auto *kd = this->Get(ci); if (kd == NULL) return; data.Store("kickerdata:amsgs", kd->amsgs); data.Store("kickerdata:badwords", kd->badwords); data.Store("kickerdata:bolds", kd->bolds); data.Store("kickerdata:caps", kd->caps); data.Store("kickerdata:colors", kd->colors); data.Store("kickerdata:flood", kd->flood); data.Store("kickerdata:italics", kd->italics); data.Store("kickerdata:repeat", kd->repeat); data.Store("kickerdata:reverses", kd->reverses); data.Store("kickerdata:underlines", kd->underlines); data.Store("capsmin", kd->capsmin); data.Store("capspercent", kd->capspercent); data.Store("floodlines", kd->floodlines); data.Store("floodsecs", kd->floodsecs); data.Store("repeattimes", kd->repeattimes); data.Store("dontkickops", kd->dontkickops); data.Store("dontkickvoices", kd->dontkickvoices); std::ostringstream oss; for (auto ttbtype : kd->ttb) oss << ttbtype << " "; data.Store("ttb", oss.str()); } void ExtensibleUnserialize(Extensible *e, Serializable *s, Serialize::Data &data) override { if (s->GetSerializableType()->GetName() != CHANNELINFO_TYPE) return; auto *ci = anope_dynamic_static_cast(e); auto *kd = ci->Require(BOTSERV_KICKER_DATA_EXT); kd->amsgs = data.Load("kickerdata:amsgs"); kd->badwords = data.Load("kickerdata:badwords"); kd->bolds = data.Load("kickerdata:bolds"); kd->caps = data.Load("kickerdata:caps"); kd->colors = data.Load("kickerdata:colors"); kd->flood = data.Load("kickerdata:flood"); kd->italics = data.Load("kickerdata:italics"); kd->repeat = data.Load("kickerdata:repeat"); kd->reverses = data.Load("kickerdata:reverses"); kd->underlines = data.Load("kickerdata:underlines"); kd->capsmin = data.Load("capsmin"); kd->capspercent = data.Load("capspercent"); kd->floodlines = data.Load("floodlines"); kd->floodsecs = data.Load("floodsecs"); kd->repeattimes = data.Load("repeattimes"); kd->dontkickops = data.Load("dontkickops"); kd->dontkickvoices = data.Load("dontkickvoices"); spacesepstream sep(data.Load("ttb")); for (size_t i = 0; i < BotServ::TTB_SIZE; ++i) { Anope::string tok; if (!sep.GetToken(tok)) break; if (auto n = Anope::TryConvert(tok)) kd->ttb[i] = n.value(); } kd->Check(ci); } }; }; class CommandBSKick final : public Command { public: CommandBSKick(Module *creator) : Command(creator, "botserv/kick", 0) { this->SetDesc(_("Configures kickers")); this->SetSyntax(_("\037option\037 \037channel\037 {\037ON|OFF\037} [\037settings\037]")); } void Execute(CommandSource &source, const std::vector ¶ms) override { this->OnSyntaxError(source, ""); } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_("Configures bot kickers. \037option\037 can be one of:")); HelpWrapper help; Anope::string this_name = source.command; for (const auto &[c_name, info] : source.service->commands) { if (c_name.find_ci(this_name + " ") == 0) { ServiceReference command("Command", info.name); if (command) { source.command = c_name; command->OnServHelp(source, help); } } } help.SendTo(source); source.Reply(_( "Type \002%s\033\037option\037\002 for more information " "on a specific option." "\n\n" "Note: access to this command is controlled by the " "level SET." ), source.service->GetQueryCommand("generic/help", this_name).c_str()); return true; } }; class CommandBSKickBase : public Command { public: CommandBSKickBase(Module *creator, const Anope::string &cname, int minarg, int maxarg) : Command(creator, cname, minarg, maxarg) { } void Execute(CommandSource &source, const std::vector ¶ms) override = 0; bool OnHelp(CommandSource &source, const Anope::string &subcommand) override = 0; protected: bool CheckArguments(CommandSource &source, const std::vector ¶ms, ChannelInfo *&ci) { const Anope::string &chan = params[0]; const Anope::string &option = params[1]; ci = ChannelInfo::Find(chan); if (Anope::ReadOnly) source.Reply(READ_ONLY_MODE); else if (ci == NULL) source.Reply(CHAN_X_NOT_REGISTERED, params[0].c_str()); else if (option.empty()) this->OnSyntaxError(source, ""); else if (!option.equals_ci("ON") && !option.equals_ci("OFF")) this->OnSyntaxError(source, ""); else if (!source.AccessFor(ci).HasPriv("SET") && !source.HasPriv("botserv/administration")) source.Reply(ACCESS_DENIED); else if (!ci->bi) source.Reply(BOT_NOT_ASSIGNED); else return true; return false; } void Process(CommandSource &source, ChannelInfo *ci, const Anope::string ¶m, const Anope::string &ttb, size_t ttb_idx, const Anope::string &optname, BotServ::KickerData *kd, bool &val) { if (param.equals_ci("ON")) { if (!ttb.empty()) { kd->ttb[ttb_idx] = Anope::Convert(ttb, -1); if (kd->ttb[ttb_idx] < 0) { kd->ttb[ttb_idx] = 0; source.Reply(_("\002%s\002 cannot be taken as times to ban."), ttb.c_str()); return; } } else kd->ttb[ttb_idx] = 0; val = true; if (kd->ttb[ttb_idx]) { source.Reply(_("Bot will now kick for \002%s\002, and will place a ban after %d kicks for the same user."), optname.c_str(), kd->ttb[ttb_idx]); } else source.Reply(_("Bot will now kick for \002%s\002."), optname.c_str()); bool override = !source.AccessFor(ci).HasPriv("SET"); Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to enable the " << optname << " kicker"; } else if (param.equals_ci("OFF")) { bool override = !source.AccessFor(ci).HasPriv("SET"); Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to disable the " << optname << " kicker"; val = false; source.Reply(_("Bot won't kick for \002%s\002 anymore."), optname.c_str()); } else this->OnSyntaxError(source, ""); } }; class CommandBSKickAMSG final : public CommandBSKickBase { public: CommandBSKickAMSG(Module *creator) : CommandBSKickBase(creator, "botserv/kick/amsg", 2, 3) { this->SetDesc(_("Configures AMSG kicker")); this->SetSyntax(_("\037channel\037 {\037ON|OFF\037} [\037ttb\037]")); } void Execute(CommandSource &source, const std::vector ¶ms) override { ChannelInfo *ci; if (CheckArguments(source, params, ci)) { auto *kd = ci->Require(BOTSERV_KICKER_DATA_EXT); Process(source, ci, params[1], params.size() > 2 ? params[2] : "", BotServ::TTB_AMSGS, "AMSG", kd, kd->amsgs); kd->Check(ci); } } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { this->SendSyntax(source); source.Reply(" "); BotInfo *bi = Config->GetClient("BotServ"); source.Reply(_( "Sets the AMSG kicker on or off. When enabled, the bot will " "kick users who send the same message to multiple channels " "where %s bots are." "\n\n" "\037ttb\037 is the number of times a user can be kicked " "before they get banned. Don't give ttb to disable " "the ban system once activated." ), bi ? bi->nick.c_str() : "BotServ"); return true; } }; class CommandBSKickBadwords final : public CommandBSKickBase { public: CommandBSKickBadwords(Module *creator) : CommandBSKickBase(creator, "botserv/kick/badwords", 2, 3) { this->SetDesc(_("Configures badwords kicker")); this->SetSyntax(_("\037channel\037 {\037ON|OFF\037} [\037ttb\037]")); } void Execute(CommandSource &source, const std::vector ¶ms) override { ChannelInfo *ci; if (CheckArguments(source, params, ci)) { auto *kd = ci->Require(BOTSERV_KICKER_DATA_EXT); Process(source, ci, params[1], params.size() > 2 ? params[2] : "", BotServ::TTB_BADWORDS, BOTSERV_BAD_WORDS_EXT, kd, kd->badwords); kd->Check(ci); } } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_( "Sets the bad words kicker on or off. When enabled, this " "option tells the bot to kick users who say certain words " "on the channels." "\n\n" "You can define bad words for your channel using the " "\002BADWORDS\002 command. Type \002%s\033BADWORDS\002 for " "more information." "\n\n" "\037ttb\037 is the number of times a user can be kicked " "before it gets banned. Don't give ttb to disable " "the ban system once activated." ), source.service->GetQueryCommand("generic/help").c_str()); return true; } }; class CommandBSKickBolds final : public CommandBSKickBase { public: CommandBSKickBolds(Module *creator) : CommandBSKickBase(creator, "botserv/kick/bolds", 2, 3) { this->SetDesc(_("Configures bolds kicker")); this->SetSyntax(_("\037channel\037 {\037ON|OFF\037} [\037ttb\037]")); } void Execute(CommandSource &source, const std::vector ¶ms) override { ChannelInfo *ci; if (CheckArguments(source, params, ci)) { auto *kd = ci->Require(BOTSERV_KICKER_DATA_EXT); Process(source, ci, params[1], params.size() > 2 ? params[2] : "", BotServ::TTB_BOLDS, "bolds", kd, kd->bolds); kd->Check(ci); } } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_( "Sets the bolds kicker on or off. When enabled, this " "option tells the bot to kick users who use bolds." "\n\n" "\037ttb\037 is the number of times a user can be kicked " "before it gets banned. Don't give ttb to disable " "the ban system once activated." )); return true; } }; class CommandBSKickCaps final : public CommandBSKickBase { public: CommandBSKickCaps(Module *creator) : CommandBSKickBase(creator, "botserv/kick/caps", 2, 5) { this->SetDesc(_("Configures caps kicker")); this->SetSyntax(_("\037channel\037 {\037ON|OFF\037} [\037ttb\037 [\037min\037 [\037percent\037]]]\002")); } void Execute(CommandSource &source, const std::vector ¶ms) override { ChannelInfo *ci; if (!CheckArguments(source, params, ci)) return; auto *kd = ci->Require(BOTSERV_KICKER_DATA_EXT); if (params[1].equals_ci("ON")) { const Anope::string &ttb = params.size() > 2 ? params[2] : "", &min = params.size() > 3 ? params[3] : "", &percent = params.size() > 4 ? params[4] : ""; if (!ttb.empty()) { kd->ttb[BotServ::TTB_CAPS] = Anope::Convert(ttb, -1); if (kd->ttb[BotServ::TTB_CAPS] < 0) { kd->ttb[BotServ::TTB_CAPS] = 0; source.Reply(_("\002%s\002 cannot be taken as times to ban."), ttb.c_str()); return; } } else kd->ttb[BotServ::TTB_CAPS] = 0; kd->capsmin = Anope::Convert(min, 0); if (kd->capsmin < 1) kd->capsmin = 10; kd->capspercent = Anope::Convert(percent, 0); if (kd->capspercent < 1 || kd->capspercent > 100) kd->capspercent = 25; kd->caps = true; if (kd->ttb[BotServ::TTB_CAPS]) { source.Reply(_( "Bot will now kick for \002caps\002 (they must constitute at least " "%d characters and %d%% of the entire message), and will " "place a ban after %d kicks for the same user." ), kd->capsmin, kd->capspercent, kd->ttb[BotServ::TTB_CAPS]); } else source.Reply(_( "Bot will now kick for \002caps\002 (they must constitute at least " "%d characters and %d%% of the entire message)." ), kd->capsmin, kd->capspercent); } else { kd->caps = false; source.Reply(_("Bot won't kick for \002caps\002 anymore.")); } kd->Check(ci); } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_( "Sets the caps kicker on or off. When enabled, this " "option tells the bot to kick users who are talking in " "CAPS." "\n\n" "The bot kicks only if there are at least \002min\002 caps " "and they constitute at least \002percent\002%% of the total " "text line (if not given, it defaults to 10 characters " "and 25%%)." "\n\n" "\037ttb\037 is the number of times a user can be kicked " "before it gets banned. Don't give ttb to disable " "the ban system once activated." )); return true; } }; class CommandBSKickColors final : public CommandBSKickBase { public: CommandBSKickColors(Module *creator) : CommandBSKickBase(creator, "botserv/kick/colors", 2, 3) { this->SetDesc(_("Configures color kicker")); this->SetSyntax(_("\037channel\037 {\037ON|OFF\037} [\037ttb\037]")); } void Execute(CommandSource &source, const std::vector ¶ms) override { ChannelInfo *ci; if (CheckArguments(source, params, ci)) { auto *kd = ci->Require(BOTSERV_KICKER_DATA_EXT); Process(source, ci, params[1], params.size() > 2 ? params[2] : "", BotServ::TTB_COLORS, "colors", kd, kd->colors); kd->Check(ci); } } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_( "Sets the colors kicker on or off. When enabled, this " "option tells the bot to kick users who use colors." "\n\n" "\037ttb\037 is the number of times a user can be kicked " "before it gets banned. Don't give ttb to disable " "the ban system once activated." )); return true; } }; class CommandBSKickFlood final : public CommandBSKickBase { public: CommandBSKickFlood(Module *creator) : CommandBSKickBase(creator, "botserv/kick/flood", 2, 5) { this->SetDesc(_("Configures flood kicker")); this->SetSyntax(_("\037channel\037 {\037ON|OFF\037} [\037ttb\037 [\037ln\037 [\037secs\037]]]\002")); } void Execute(CommandSource &source, const std::vector ¶ms) override { ChannelInfo *ci; if (!CheckArguments(source, params, ci)) return; auto *kd = ci->Require(BOTSERV_KICKER_DATA_EXT); if (params[1].equals_ci("ON")) { const Anope::string &ttb = params.size() > 2 ? params[2] : "", &lines = params.size() > 3 ? params[3] : "", &secs = params.size() > 4 ? params[4] : ""; if (!ttb.empty()) { kd->ttb[BotServ::TTB_FLOOD] = Anope::Convert(ttb, -1); if (kd->ttb[BotServ::TTB_FLOOD] < 0) { kd->ttb[BotServ::TTB_FLOOD] = 0; source.Reply(_("\002%s\002 cannot be taken as times to ban."), ttb.c_str()); return; } } else kd->ttb[BotServ::TTB_FLOOD] = 0; kd->floodlines = Anope::Convert(lines, -1); if (kd->floodlines < 2) kd->floodlines = 6; kd->floodsecs = Anope::Convert(secs, -1); if (kd->floodsecs < 1) kd->floodsecs = 10; if (kd->floodsecs > Config->GetModule(me).Get("keepdata")) kd->floodsecs = Config->GetModule(me).Get("keepdata"); kd->flood = true; if (kd->ttb[BotServ::TTB_FLOOD]) { source.Reply(_( "Bot will now kick for \002flood\002 (%d lines in %d seconds " "and will place a ban after %d kicks for the same user." ), kd->floodlines, kd->floodsecs, kd->ttb[BotServ::TTB_FLOOD]); } else source.Reply(_("Bot will now kick for \002flood\002 (%d lines in %d seconds)."), kd->floodlines, kd->floodsecs); } else if (params[1].equals_ci("OFF")) { kd->flood = false; source.Reply(_("Bot won't kick for \002flood\002 anymore.")); } else this->OnSyntaxError(source, params[1]); kd->Check(ci); } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_( "Sets the flood kicker on or off. When enabled, this " "option tells the bot to kick users who are flooding " "the channel using at least \002ln\002 lines in \002secs\002 seconds " "(if not given, it defaults to 6 lines in 10 seconds). " "\n\n" "\037ttb\037 is the number of times a user can be kicked " "before it gets banned. Don't give ttb to disable " "the ban system once activated.")); return true; } }; class CommandBSKickItalics final : public CommandBSKickBase { public: CommandBSKickItalics(Module *creator) : CommandBSKickBase(creator, "botserv/kick/italics", 2, 3) { this->SetDesc(_("Configures italics kicker")); this->SetSyntax(_("\037channel\037 {\037ON|OFF\037} [\037ttb\037]")); } void Execute(CommandSource &source, const std::vector ¶ms) override { ChannelInfo *ci; if (CheckArguments(source, params, ci)) { auto *kd = ci->Require(BOTSERV_KICKER_DATA_EXT); Process(source, ci, params[1], params.size() > 2 ? params[2] : "", BotServ::TTB_ITALICS, "italics", kd, kd->italics); kd->Check(ci); } } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_( "Sets the italics kicker on or off. When enabled, this " "option tells the bot to kick users who use italics. " "\n\n" "\037ttb\037 is the number of times a user can be kicked " "before it gets banned. Don't give ttb to disable " "the ban system once activated." )); return true; } }; class CommandBSKickRepeat final : public CommandBSKickBase { public: CommandBSKickRepeat(Module *creator) : CommandBSKickBase(creator, "botserv/kick/repeat", 2, 4) { this->SetDesc(_("Configures repeat kicker")); this->SetSyntax(_("\037channel\037 {\037ON|OFF\037} [\037ttb\037 [\037num\037]]\002")); } void Execute(CommandSource &source, const std::vector ¶ms) override { ChannelInfo *ci; if (!CheckArguments(source, params, ci)) return; auto *kd = ci->Require(BOTSERV_KICKER_DATA_EXT); if (params[1].equals_ci("ON")) { const Anope::string &ttb = params.size() > 2 ? params[2] : "", × = params.size() > 3 ? params[3] : ""; if (!ttb.empty()) { kd->ttb[BotServ::TTB_REPEAT] = Anope::Convert(ttb, -1); if (kd->ttb[BotServ::TTB_REPEAT] < 0) { kd->ttb[BotServ::TTB_REPEAT] = 0; source.Reply(_("\002%s\002 cannot be taken as times to ban."), ttb.c_str()); return; } } else kd->ttb[BotServ::TTB_REPEAT] = 0; kd->repeattimes = Anope::Convert(times, -1); if (kd->repeattimes < 1) kd->repeattimes = 3; kd->repeat = true; if (kd->ttb[BotServ::TTB_REPEAT]) { if (kd->repeattimes != 1) { source.Reply(_( "Bot will now kick for \002repeats\002 (users that repeat the " "same message %d times), and will place a ban after %d " "kicks for the same user." ), kd->repeattimes, kd->ttb[BotServ::TTB_REPEAT]); } else source.Reply(_( "Bot will now kick for \002repeats\002 (users that repeat the " "same message %d time), and will place a ban after %d " "kicks for the same user." ), kd->repeattimes, kd->ttb[BotServ::TTB_REPEAT]); } else { if (kd->repeattimes != 1) { source.Reply(_( "Bot will now kick for \002repeats\002 (users that repeat the " "same message %d times)." ), kd->repeattimes); } else { source.Reply(_( "Bot will now kick for \002repeats\002 (users that repeat the " "same message %d time)." ), kd->repeattimes); } } } else if (params[1].equals_ci("OFF")) { kd->repeat = false; source.Reply(_("Bot won't kick for \002repeats\002 anymore.")); } else this->OnSyntaxError(source, params[1]); kd->Check(ci); } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_( "Sets the repeat kicker on or off. When enabled, this " "option tells the bot to kick users who are repeating " "themselves \002num\002 times (if num is not given, it " "defaults to 3)." "\n\n" "\037ttb\037 is the number of times a user can be kicked " "before it gets banned. Don't give ttb to disable " "the ban system once activated." )); return true; } }; class CommandBSKickReverses final : public CommandBSKickBase { public: CommandBSKickReverses(Module *creator) : CommandBSKickBase(creator, "botserv/kick/reverses", 2, 3) { this->SetDesc(_("Configures reverses kicker")); this->SetSyntax(_("\037channel\037 {\037ON|OFF\037} [\037ttb\037]")); } void Execute(CommandSource &source, const std::vector ¶ms) override { ChannelInfo *ci; if (CheckArguments(source, params, ci)) { auto *kd = ci->Require(BOTSERV_KICKER_DATA_EXT); Process(source, ci, params[1], params.size() > 2 ? params[2] : "", BotServ::TTB_REVERSES, "reverses", kd, kd->reverses); kd->Check(ci); } } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_( "Sets the reverses kicker on or off. When enabled, this " "option tells the bot to kick users who use reverses. " "\n\n" "\037ttb\037 is the number of times a user can be kicked " "before it gets banned. Don't give ttb to disable " "the ban system once activated." )); return true; } }; class CommandBSKickUnderlines final : public CommandBSKickBase { public: CommandBSKickUnderlines(Module *creator) : CommandBSKickBase(creator, "botserv/kick/underlines", 2, 3) { this->SetDesc(_("Configures underlines kicker")); this->SetSyntax(_("\037channel\037 {\037ON|OFF\037} [\037ttb\037]")); } void Execute(CommandSource &source, const std::vector ¶ms) override { ChannelInfo *ci; if (CheckArguments(source, params, ci)) { auto *kd = ci->Require(BOTSERV_KICKER_DATA_EXT); Process(source, ci, params[1], params.size() > 2 ? params[2] : "", BotServ::TTB_UNDERLINES, "underlines", kd, kd->underlines); kd->Check(ci); } } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_( "Sets the underlines kicker on or off. When enabled, this " "option tells the bot to kick users who use underlines. " "\n\n" "\037ttb\037 is the number of times a user can be kicked " "before it gets banned. Don't give ttb to disable " "the ban system once activated." )); return true; } }; class CommandBSSetDontKickOps final : public Command { public: CommandBSSetDontKickOps(Module *creator, const Anope::string &sname = "botserv/set/dontkickops") : Command(creator, sname, 2, 2) { this->SetDesc(_("To protect ops against bot kicks")); this->SetSyntax(_("\037channel\037 {ON | OFF}")); } void Execute(CommandSource &source, const std::vector ¶ms) override { ChannelInfo *ci = ChannelInfo::Find(params[0]); if (ci == NULL) { source.Reply(CHAN_X_NOT_REGISTERED, params[0].c_str()); return; } AccessGroup access = source.AccessFor(ci); if (!source.HasPriv("botserv/administration") && !access.HasPriv("SET")) { source.Reply(ACCESS_DENIED); return; } if (Anope::ReadOnly) { source.Reply(READ_ONLY_MODE); return; } auto *kd = ci->Require(BOTSERV_KICKER_DATA_EXT); if (params[1].equals_ci("ON")) { bool override = !access.HasPriv("SET"); Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to enable dontkickops"; kd->dontkickops = true; source.Reply(_("Bot \002won't kick ops\002 on channel %s."), ci->name.c_str()); } else if (params[1].equals_ci("OFF")) { bool override = !access.HasPriv("SET"); Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to disable dontkickops"; kd->dontkickops = false; source.Reply(_("Bot \002will kick ops\002 on channel %s."), ci->name.c_str()); } else this->OnSyntaxError(source, source.command); kd->Check(ci); } bool OnHelp(CommandSource &source, const Anope::string &) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_( "Enables or disables \002ops protection\002 mode on a channel. " "When it is enabled, ops won't be kicked by the bot " "even if they don't match the NOKICK level." )); return true; } }; class CommandBSSetDontKickVoices final : public Command { public: CommandBSSetDontKickVoices(Module *creator, const Anope::string &sname = "botserv/set/dontkickvoices") : Command(creator, sname, 2, 2) { this->SetDesc(_("To protect voices against bot kicks")); this->SetSyntax(_("\037channel\037 {ON | OFF}")); } void Execute(CommandSource &source, const std::vector ¶ms) override { ChannelInfo *ci = ChannelInfo::Find(params[0]); if (ci == NULL) { source.Reply(CHAN_X_NOT_REGISTERED, params[0].c_str()); return; } AccessGroup access = source.AccessFor(ci); if (!source.HasPriv("botserv/administration") && !access.HasPriv("SET")) { source.Reply(ACCESS_DENIED); return; } if (Anope::ReadOnly) { source.Reply(READ_ONLY_MODE); return; } auto *kd = ci->Require(BOTSERV_KICKER_DATA_EXT); if (params[1].equals_ci("ON")) { bool override = !access.HasPriv("SET"); Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to enable dontkickvoices"; kd->dontkickvoices = true; source.Reply(_("Bot \002won't kick voices\002 on channel %s."), ci->name.c_str()); } else if (params[1].equals_ci("OFF")) { bool override = !access.HasPriv("SET"); Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to disable dontkickvoices"; kd->dontkickvoices = false; source.Reply(_("Bot \002will kick voices\002 on channel %s."), ci->name.c_str()); } else this->OnSyntaxError(source, source.command); kd->Check(ci); } bool OnHelp(CommandSource &source, const Anope::string &) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_( "Enables or disables \002voices protection\002 mode on a channel. " "When it is enabled, voices won't be kicked by the bot " "even if they don't match the NOKICK level." )); return true; } }; struct BanData final { struct Data final { Anope::string mask; time_t last_use; int16_t ttb[BotServ::TTB_SIZE]; Data() { last_use = 0; for (auto &ttbtype : this->ttb) ttbtype = 0; } }; private: typedef Anope::map data_type; data_type data_map; public: BanData(Extensible *) { } Data &get(const Anope::string &key) { return this->data_map[key]; } bool empty() const { return this->data_map.empty(); } void purge() { time_t keepdata = Config->GetModule(me).Get("keepdata"); for (auto it = data_map.begin(), it_end = data_map.end(); it != it_end;) { const Anope::string &user = it->first; Data &bd = it->second; ++it; if (Anope::CurTime - bd.last_use > keepdata) data_map.erase(user); } } }; struct UserData final { UserData(Extensible *) { last_use = last_start = Anope::CurTime; lines = times = 0; lastline.clear(); } /* Data validity */ time_t last_use; /* for flood kicker */ int16_t lines; time_t last_start; /* for repeat kicker */ Anope::string lasttarget; int16_t times; Anope::string lastline; }; class BanDataPurger final : public Timer { public: BanDataPurger(Module *o) : Timer(o, 300, true) { } void Tick() override { Log(LOG_DEBUG) << "bs_main: Running bandata purger"; for (auto &[_, c] : ChannelList) { auto *bd = c->GetExt("bandata"); if (bd != NULL) { bd->purge(); if (bd->empty()) c->Shrink("bandata"); } } } }; class BSKick final : public Module { ExtensibleItem bandata; ExtensibleItem userdata; KickerDataImpl::ExtensibleItem kickerdata; CommandBSKick commandbskick; CommandBSKickAMSG commandbskickamsg; CommandBSKickBadwords commandbskickbadwords; CommandBSKickBolds commandbskickbolds; CommandBSKickCaps commandbskickcaps; CommandBSKickColors commandbskickcolors; CommandBSKickFlood commandbskickflood; CommandBSKickItalics commandbskickitalics; CommandBSKickRepeat commandbskickrepeat; CommandBSKickReverses commandbskickreverse; CommandBSKickUnderlines commandbskickunderlines; CommandBSSetDontKickOps commandbssetdontkickops; CommandBSSetDontKickVoices commandbssetdontkickvoices; BanDataPurger purger; BanData::Data &GetBanData(User *u, Channel *c) { BanData *bd = bandata.Require(c); return bd->get(u->GetMask()); } UserData *GetUserData(User *u, Channel *c) { auto *memb = c->FindUser(u); if (memb == NULL) return NULL; UserData *ud = userdata.Require(memb); return ud; } void check_ban(ChannelInfo *ci, User *u, BotServ::KickerData *kd, int ttbtype) { /* Don't ban ulines or protected users */ if (u->IsProtected()) return; BanData::Data &bd = this->GetBanData(u, ci->c); ++bd.ttb[ttbtype]; if (kd->ttb[ttbtype] && bd.ttb[ttbtype] >= kd->ttb[ttbtype]) { /* Should not use == here because bd.ttb[ttbtype] could possibly be > kd->ttb[ttbtype] * if the TTB was changed after it was not set (0) before and the user had already been * kicked a few times. Bug #1056 - Adam */ bd.ttb[ttbtype] = 0; Anope::string mask = ci->GetIdealBan(u); ci->c->SetMode(NULL, "BAN", mask); FOREACH_MOD(OnBotBan, (u, ci, mask)); } } static void bot_kick(ChannelInfo *ci, User *u, const char *message, ...) ATTR_FORMAT(3, 4) { if (!ci || !ci->bi || !ci->c || !u || u->IsProtected() || !ci->c->FindUser(u)) return; const auto *fmt = Language::Translate(u, message); Anope::string buf; ANOPE_FORMAT(message, fmt, buf); ci->c->Kick(ci->bi, u, buf); } public: BSKick(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), bandata(this, "bandata"), userdata(this, "userdata"), kickerdata(this, BOTSERV_KICKER_DATA_EXT), commandbskick(this), commandbskickamsg(this), commandbskickbadwords(this), commandbskickbolds(this), commandbskickcaps(this), commandbskickcolors(this), commandbskickflood(this), commandbskickitalics(this), commandbskickrepeat(this), commandbskickreverse(this), commandbskickunderlines(this), commandbssetdontkickops(this), commandbssetdontkickvoices(this), purger(this) { me = this; } void OnBotInfo(CommandSource &source, BotInfo *bi, ChannelInfo *ci, InfoFormatter &info) override { if (!ci) return; Anope::string enabled = Language::Translate(source.nc, _("Enabled")); Anope::string disabled = Language::Translate(source.nc, _("Disabled")); auto *kd = kickerdata.Get(ci); if (kd && kd->badwords) { if (kd->ttb[BotServ::TTB_BADWORDS]) info[_("Bad words kicker")] = Anope::Format("%s (%d kick(s) to ban)", enabled.c_str(), kd->ttb[BotServ::TTB_BADWORDS]); else info[_("Bad words kicker")] = enabled; } else info[_("Bad words kicker")] = disabled; if (kd && kd->bolds) { if (kd->ttb[BotServ::TTB_BOLDS]) info[_("Bolds kicker")] = Anope::Format("%s (%d kick(s) to ban)", enabled.c_str(), kd->ttb[BotServ::TTB_BOLDS]); else info[_("Bolds kicker")] = enabled; } else info[_("Bolds kicker")] = disabled; if (kd && kd->caps) { if (kd->ttb[BotServ::TTB_CAPS]) info[_("Caps kicker")] = Anope::Format(_("%s (%d kick(s) to ban; minimum %d/%d%%)"), enabled.c_str(), kd->ttb[BotServ::TTB_CAPS], kd->capsmin, kd->capspercent); else info[_("Caps kicker")] = Anope::Format(_("%s (minimum %d/%d%%)"), enabled.c_str(), kd->capsmin, kd->capspercent); } else info[_("Caps kicker")] = disabled; if (kd && kd->colors) { if (kd->ttb[BotServ::TTB_COLORS]) info[_("Colors kicker")] = Anope::Format(_("%s (%d kick(s) to ban)"), enabled.c_str(), kd->ttb[BotServ::TTB_COLORS]); else info[_("Colors kicker")] = enabled; } else info[_("Colors kicker")] = disabled; if (kd && kd->flood) { if (kd->ttb[BotServ::TTB_FLOOD]) info[_("Flood kicker")] = Anope::Format(_("%s (%d kick(s) to ban; %d lines in %ds)"), enabled.c_str(), kd->ttb[BotServ::TTB_FLOOD], kd->floodlines, kd->floodsecs); else info[_("Flood kicker")] = Anope::Format(_("%s (%d lines in %ds)"), enabled.c_str(), kd->floodlines, kd->floodsecs); } else info[_("Flood kicker")] = disabled; if (kd && kd->repeat) { if (kd->ttb[BotServ::TTB_REPEAT]) info[_("Repeat kicker")] = Anope::Format(_("%s (%d kick(s) to ban; %d times)"), enabled.c_str(), kd->ttb[BotServ::TTB_REPEAT], kd->repeattimes); else info[_("Repeat kicker")] = Anope::Format(_("%s (%d times)"), enabled.c_str(), kd->repeattimes); } else info[_("Repeat kicker")] = disabled; if (kd && kd->reverses) { if (kd->ttb[BotServ::TTB_REVERSES]) info[_("Reverses kicker")] = Anope::Format(_("%s (%d kick(s) to ban)"), enabled.c_str(), kd->ttb[BotServ::TTB_REVERSES]); else info[_("Reverses kicker")] = enabled; } else info[_("Reverses kicker")] = disabled; if (kd && kd->underlines) { if (kd->ttb[BotServ::TTB_UNDERLINES]) info[_("Underlines kicker")] = Anope::Format(_("%s (%d kick(s) to ban)"), enabled.c_str(), kd->ttb[BotServ::TTB_UNDERLINES]); else info[_("Underlines kicker")] = enabled; } else info[_("Underlines kicker")] = disabled; if (kd && kd->italics) { if (kd->ttb[BotServ::TTB_ITALICS]) info[_("Italics kicker")] = Anope::Format(_("%s (%d kick(s) to ban)"), enabled.c_str(), kd->ttb[BotServ::TTB_ITALICS]); else info[_("Italics kicker")] = enabled; } else info[_("Italics kicker")] = disabled; if (kd && kd->amsgs) { if (kd->ttb[BotServ::TTB_AMSGS]) info[_("AMSG kicker")] = Anope::Format(_("%s (%d kick(s) to ban)"), enabled.c_str(), kd->ttb[BotServ::TTB_AMSGS]); else info[_("AMSG kicker")] = enabled; } else info[_("AMSG kicker")] = disabled; if (kd && kd->dontkickops) info.AddOption(_("Ops protection")); if (kd && kd->dontkickvoices) info.AddOption(_("Voices protection")); } void OnPrivmsg(User *u, Channel *c, Anope::string &msg, const Anope::map &tags) override { /* Now we can make kicker stuff. We try to order the checks * from the fastest one to the slowest one, since there's * no need to process other kickers if a user is kicked before * the last kicker check. * * But FIRST we check whether the user is protected in any * way. */ ChannelInfo *ci = c->ci; if (ci == NULL) return; auto *kd = kickerdata.Get(ci); if (kd == NULL) return; if (ci->AccessFor(u).HasPriv("NOKICK")) return; else if (kd->dontkickops && (c->HasUserStatus(u, "HALFOP") || c->HasUserStatus(u, "OP") || c->HasUserStatus(u, "PROTECT") || c->HasUserStatus(u, "OWNER"))) return; else if (kd->dontkickvoices && c->HasUserStatus(u, "VOICE")) return; Anope::string realbuf = msg; /* If it's a /me, cut the CTCP part because the ACTION will cause * problems with the caps or badwords kicker */ Anope::string ctcpname, ctcpbody; if (Anope::ParseCTCP(msg, ctcpname, ctcpbody) && ctcpname.equals_ci("ACTION")) realbuf = ctcpbody; if (realbuf.empty()) return; /* Bolds kicker */ if (kd->bolds && realbuf.find(2) != Anope::string::npos) { check_ban(ci, u, kd, BotServ::TTB_BOLDS); bot_kick(ci, u, _("Don't use bolds on this channel!")); return; } /* Color kicker */ if (kd->colors && realbuf.find(3) != Anope::string::npos) { check_ban(ci, u, kd, BotServ::TTB_COLORS); bot_kick(ci, u, _("Don't use colors on this channel!")); return; } /* Reverses kicker */ if (kd->reverses && realbuf.find(22) != Anope::string::npos) { check_ban(ci, u, kd, BotServ::TTB_REVERSES); bot_kick(ci, u, _("Don't use reverses on this channel!")); return; } /* Italics kicker */ if (kd->italics && realbuf.find(29) != Anope::string::npos) { check_ban(ci, u, kd, BotServ::TTB_ITALICS); bot_kick(ci, u, _("Don't use italics on this channel!")); return; } /* Underlines kicker */ if (kd->underlines && realbuf.find(31) != Anope::string::npos) { check_ban(ci, u, kd, BotServ::TTB_UNDERLINES); bot_kick(ci, u, _("Don't use underlines on this channel!")); return; } /* Caps kicker */ if (kd->caps && realbuf.length() >= static_cast(kd->capsmin)) { int i = 0, l = 0; for (auto chr : realbuf) { if (isupper(chr)) ++i; else if (islower(chr)) ++l; } /* i counts uppercase chars, l counts lowercase chars. Only * alphabetic chars (so islower || isupper) qualify for the * percentage of caps to kick for; the rest is ignored. -GD */ if ((i || l) && i >= kd->capsmin && i * 100 / (i + l) >= kd->capspercent) { check_ban(ci, u, kd, BotServ::TTB_CAPS); bot_kick(ci, u, _("Turn caps lock OFF!")); return; } } /* Bad words kicker */ if (kd->badwords) { bool mustkick = false; auto *badwords = ci->GetExt(BOTSERV_BAD_WORDS_EXT); /* Normalize the buffer */ Anope::string nbuf = Anope::RemoveFormatting(realbuf); bool casesensitive = Config->GetModule("botserv").Get("casesensitive"); /* Normalize can return an empty string if this only contains control codes etc */ if (badwords && !nbuf.empty()) for (unsigned i = 0; i < badwords->GetBadWordCount(); ++i) { const auto *bw = badwords->GetBadWord(i); if (bw->word.empty()) continue; // Shouldn't happen if (bw->word.length() > nbuf.length()) continue; // This can't ever match if (bw->type == BotServ::BW_ANY && ((casesensitive && nbuf.find(bw->word) != Anope::string::npos) || (!casesensitive && nbuf.find_ci(bw->word) != Anope::string::npos))) mustkick = true; else if (bw->type == BotServ::BW_SINGLE) { size_t len = bw->word.length(); if ((casesensitive && bw->word.equals_cs(nbuf)) || (!casesensitive && bw->word.equals_ci(nbuf))) mustkick = true; else if (nbuf.find(' ') == len && ((casesensitive && bw->word.equals_cs(nbuf.substr(0, len))) || (!casesensitive && bw->word.equals_ci(nbuf.substr(0, len))))) mustkick = true; else { if (len < nbuf.length() && nbuf.rfind(' ') == nbuf.length() - len - 1 && ((casesensitive && nbuf.find(bw->word) == nbuf.length() - len) || (!casesensitive && nbuf.find_ci(bw->word) == nbuf.length() - len))) mustkick = true; else { Anope::string wordbuf = " " + bw->word + " "; if ((casesensitive && nbuf.find(wordbuf) != Anope::string::npos) || (!casesensitive && nbuf.find_ci(wordbuf) != Anope::string::npos)) mustkick = true; } } } else if (bw->type == BotServ::BW_START) { size_t len = bw->word.length(); if ((casesensitive && nbuf.substr(0, len).equals_cs(bw->word)) || (!casesensitive && nbuf.substr(0, len).equals_ci(bw->word))) mustkick = true; else { Anope::string wordbuf = " " + bw->word; if ((casesensitive && nbuf.find(wordbuf) != Anope::string::npos) || (!casesensitive && nbuf.find_ci(wordbuf) != Anope::string::npos)) mustkick = true; } } else if (bw->type == BotServ::BW_END) { size_t len = bw->word.length(); if ((casesensitive && nbuf.substr(nbuf.length() - len).equals_cs(bw->word)) || (!casesensitive && nbuf.substr(nbuf.length() - len).equals_ci(bw->word))) mustkick = true; else { Anope::string wordbuf = bw->word + " "; if ((casesensitive && nbuf.find(wordbuf) != Anope::string::npos) || (!casesensitive && nbuf.find_ci(wordbuf) != Anope::string::npos)) mustkick = true; } } if (mustkick) { check_ban(ci, u, kd, BotServ::TTB_BADWORDS); if (Config->GetModule(me).Get("gentlebadwordreason")) bot_kick(ci, u, _("Watch your language!")); else bot_kick(ci, u, _("Don't use the word \"%s\" on this channel!"), bw->word.c_str()); return; } } /* for */ } /* if badwords */ UserData *ud = GetUserData(u, c); if (ud) { /* Flood kicker */ if (kd->flood) { if (Anope::CurTime - ud->last_start > kd->floodsecs) { ud->last_start = Anope::CurTime; ud->lines = 0; } ++ud->lines; if (ud->lines >= kd->floodlines) { check_ban(ci, u, kd, BotServ::TTB_FLOOD); bot_kick(ci, u, _("Stop flooding!")); return; } } /* Repeat kicker */ if (kd->repeat) { if (!ud->lastline.equals_ci(realbuf)) ud->times = 0; else ++ud->times; if (ud->times >= kd->repeattimes) { check_ban(ci, u, kd, BotServ::TTB_REPEAT); bot_kick(ci, u, _("Stop repeating yourself!")); return; } } if (ud->lastline.equals_ci(realbuf) && !ud->lasttarget.empty() && !ud->lasttarget.equals_ci(ci->name)) { for (auto it = u->chans.begin(); it != u->chans.end();) { Channel *chan = it->second->chan; ++it; if (chan->ci && kd->amsgs && !chan->ci->AccessFor(u).HasPriv("NOKICK")) { check_ban(chan->ci, u, kd, BotServ::TTB_AMSGS); bot_kick(chan->ci, u, _("Don't use AMSGs!")); } } } ud->lasttarget = ci->name; ud->lastline = realbuf; } } }; MODULE_INIT(BSKick)