From 484160eb4ed560eeda97c94a42dd8e31431ab251 Mon Sep 17 00:00:00 2001 From: Sadie Powell Date: Wed, 24 Jan 2024 12:34:03 +0000 Subject: Shuffle modules around a bit. --- modules/botserv/botserv.cpp | 213 ++++++ modules/botserv/bs_assign.cpp | 260 +++++++ modules/botserv/bs_autoassign.cpp | 37 + modules/botserv/bs_badwords.cpp | 476 ++++++++++++ modules/botserv/bs_bot.cpp | 387 ++++++++++ modules/botserv/bs_botlist.cpp | 82 ++ modules/botserv/bs_control.cpp | 149 ++++ modules/botserv/bs_info.cpp | 134 ++++ modules/botserv/bs_kick.cpp | 1491 +++++++++++++++++++++++++++++++++++++ modules/botserv/bs_set.cpp | 231 ++++++ 10 files changed, 3460 insertions(+) create mode 100644 modules/botserv/botserv.cpp create mode 100644 modules/botserv/bs_assign.cpp create mode 100644 modules/botserv/bs_autoassign.cpp create mode 100644 modules/botserv/bs_badwords.cpp create mode 100644 modules/botserv/bs_bot.cpp create mode 100644 modules/botserv/bs_botlist.cpp create mode 100644 modules/botserv/bs_control.cpp create mode 100644 modules/botserv/bs_info.cpp create mode 100644 modules/botserv/bs_kick.cpp create mode 100644 modules/botserv/bs_set.cpp (limited to 'modules/botserv') diff --git a/modules/botserv/botserv.cpp b/modules/botserv/botserv.cpp new file mode 100644 index 000000000..d2b8df4bb --- /dev/null +++ b/modules/botserv/botserv.cpp @@ -0,0 +1,213 @@ +/* BotServ core functions + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#include "module.h" + +class BotServCore final + : public Module +{ + Reference BotServ; + ExtensibleRef persist, inhabit; + +public: + BotServCore(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, PSEUDOCLIENT | VENDOR), + persist("PERSIST"), inhabit("inhabit") + { + } + + void OnReload(Configuration::Conf *conf) override + { + const Anope::string &bsnick = conf->GetModule(this)->Get("client"); + BotServ = BotInfo::Find(bsnick, true); + } + + void OnSetCorrectModes(User *user, Channel *chan, AccessGroup &access, bool &give_modes, bool &take_modes) override + { + /* Do not allow removing bot modes on our service bots */ + if (chan->ci && chan->ci->bi == user) + { + const Anope::string &botmodes = Config->GetModule(this)->Get("botmodes"); + for (auto botmode : botmodes) + chan->SetMode(chan->ci->bi, ModeManager::FindChannelModeByChar(botmode), chan->ci->bi->GetUID()); + } + } + + void OnBotAssign(User *sender, ChannelInfo *ci, BotInfo *bi) override + { + if (ci->c && ci->c->users.size() >= Config->GetModule(this)->Get("minusers")) + { + ChannelStatus status(Config->GetModule(this)->Get("botmodes")); + bi->Join(ci->c, &status); + } + } + + void OnJoinChannel(User *user, Channel *c) override + { + if (!Config || !IRCD) + return; + + BotInfo *bi = user->server == Me ? dynamic_cast(user) : NULL; + if (bi && Config->GetModule(this)->Get("smartjoin")) + { + /* We check for bans */ + for (const auto &entry : c->GetModeList("BAN")) + { + Entry ban("BAN", entry); + if (ban.Matches(user)) + c->RemoveMode(NULL, "BAN", ban.GetMask()); + } + + Anope::string Limit; + unsigned limit = 0; + try + { + if (c->GetParam("LIMIT", Limit)) + limit = convertTo(Limit); + } + catch (const ConvertException &) { } + + /* Should we be invited? */ + if (c->HasMode("INVITE") || (limit && c->users.size() >= limit)) + { + ChannelMode *cm = ModeManager::FindChannelModeByName("OP"); + char symbol = cm ? anope_dynamic_static_cast(cm)->symbol : 0; + IRCD->SendNotice(bi, (symbol ? Anope::string(symbol) : "") + c->name, "%s invited %s into the channel.", user->nick.c_str(), user->nick.c_str()); + } + + ModeManager::ProcessModes(); + } + + if (user->server != Me && c->ci && c->ci->bi) + { + /** + * We let the bot join even if it was an ignored user, as if we don't, + * and the ignored user doesn't just leave, the bot will never + * make it into the channel, leaving the channel botless even for + * legit users - Rob + **/ + /* This is before the user has joined the channel, so check usercount + 1 */ + if (c->users.size() + 1 >= Config->GetModule(this)->Get("minusers") && !c->FindUser(c->ci->bi)) + { + ChannelStatus status(Config->GetModule(this)->Get("botmodes")); + c->ci->bi->Join(c, &status); + } + } + } + + void OnLeaveChannel(User *u, Channel *c) override + { + /* Channel is persistent, it shouldn't be deleted and the service bot should stay */ + if (c->ci && persist && persist->HasExt(c->ci)) + return; + + /* Channel is syncing from a netburst, don't destroy it as more users are probably wanting to join immediately + * We also don't part the bot here either, if necessary we will part it after the sync + */ + if (c->syncing) + return; + + /* Additionally, do not delete this channel if ChanServ/a BotServ bot is inhabiting it */ + if (inhabit && inhabit->HasExt(c)) + return; + + /* This is called prior to removing the user from the channel, so c->users.size() - 1 should be safe */ + if (c->ci && c->ci->bi && u != *c->ci->bi && c->users.size() - 1 <= Config->GetModule(this)->Get("minusers") && c->FindUser(c->ci->bi)) + c->ci->bi->Part(c->ci->c); + } + + EventReturn OnPreHelp(CommandSource &source, const std::vector ¶ms) override + { + if (!params.empty()) + return EVENT_CONTINUE; + + if (source.c) + { + source.Reply(_("\002%s\002 allows you to execute \"fantasy\" commands in the channel.\n" + "Fantasy commands are commands that can be executed from messaging a\n" + "channel, and provide a more convenient way to execute commands. Commands that\n" + "require a channel as a parameter will automatically have that parameter\n" + "given.\n"), source.service->nick.c_str()); + const Anope::string &fantasycharacters = Config->GetModule("fantasy")->Get("fantasycharacter", "!"); + if (!fantasycharacters.empty()) + source.Reply(_(" \n" + "Fantasy commands may be prefixed with one of the following characters: %s\n"), fantasycharacters.c_str()); + source.Reply(_(" \n" + "Available commands are:")); + } + else if (*source.service == BotServ) + { + source.Reply(_("\002%s\002 allows you to have a bot on your own channel.\n" + "It has been created for users that can't host or\n" + "configure a bot, or for use on networks that don't\n" + "allow user bots. Available commands are listed\n" + "below; to use them, type \002%s%s \037command\037\002. For\n" + "more information on a specific command, type\n" + "\002%s%s %s \037command\037\002.\n"), + BotServ->nick.c_str(), Config->StrictPrivmsg.c_str(), BotServ->nick.c_str(), + Config->StrictPrivmsg.c_str(), BotServ->nick.c_str(), source.command.c_str()); + } + + return EVENT_CONTINUE; + } + + void OnPostHelp(CommandSource &source, const std::vector ¶ms) override + { + if (!params.empty() || source.c || source.service != *BotServ) + return; + + source.Reply(_(" \n" + "Bot will join a channel whenever there is at least\n" + "\002%d\002 user(s) on it."), Config->GetModule(this)->Get("minusers")); + const Anope::string &fantasycharacters = Config->GetModule("fantasy")->Get("fantasycharacter", "!"); + if (!fantasycharacters.empty()) + source.Reply(_("Additionally, if fantasy is enabled fantasy commands\n" + "can be executed by prefixing the command name with\n" + "one of the following characters: %s"), fantasycharacters.c_str()); + } + + EventReturn OnChannelModeSet(Channel *c, MessageSource &source, ChannelMode *mode, const Anope::string ¶m) override + { + if (source.GetUser() && !source.GetBot() && Config->GetModule(this)->Get("smartjoin") && mode->name == "BAN" && c->ci && c->ci->bi && c->FindUser(c->ci->bi)) + { + BotInfo *bi = c->ci->bi; + + Entry ban("BAN", param); + if (ban.Matches(bi)) + c->RemoveMode(bi, "BAN", param); + } + + return EVENT_CONTINUE; + } + + void OnCreateChan(ChannelInfo *ci) override + { + /* Set default bot flags */ + spacesepstream sep(Config->GetModule(this)->Get("defaults", "greet fantasy")); + for (Anope::string token; sep.GetToken(token);) + ci->Extend("BS_" + token.upper()); + } + + void OnUserKicked(const MessageSource &source, User *target, const Anope::string &channel, ChannelStatus &status, const Anope::string &kickmsg) override + { + BotInfo *bi = BotInfo::Find(target->GetUID()); + if (bi) + /* Bots get rejoined */ + bi->Join(channel, &status); + } + + void OnCreateBot(BotInfo *bi) override + { + if (bi->botmodes.empty()) + bi->botmodes = Config->GetModule(this)->Get("botumodes"); + } +}; + +MODULE_INIT(BotServCore) diff --git a/modules/botserv/bs_assign.cpp b/modules/botserv/bs_assign.cpp new file mode 100644 index 000000000..43abdd55f --- /dev/null +++ b/modules/botserv/bs_assign.cpp @@ -0,0 +1,260 @@ +/* BotServ core functions + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#include "module.h" + +class CommandBSAssign final + : public Command +{ +public: + CommandBSAssign(Module *creator) : Command(creator, "botserv/assign", 2, 2) + { + this->SetDesc(_("Assigns a bot to a channel")); + this->SetSyntax(_("\037channel\037 \037nick\037")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + const Anope::string &chan = params[0]; + const Anope::string &nick = params[1]; + + if (Anope::ReadOnly) + { + source.Reply(_("Sorry, bot assignment is temporarily disabled.")); + return; + } + + ChannelInfo *ci = ChannelInfo::Find(params[0]); + if (ci == NULL) + { + source.Reply(CHAN_X_NOT_REGISTERED, params[0].c_str()); + return; + } + + BotInfo *bi = BotInfo::Find(nick, true); + if (!bi) + { + source.Reply(BOT_DOES_NOT_EXIST, nick.c_str()); + return; + } + + AccessGroup access = source.AccessFor(ci); + if (ci->HasExt("BS_NOBOT") || (!access.HasPriv("ASSIGN") && !source.HasPriv("botserv/administration"))) + { + source.Reply(ACCESS_DENIED); + return; + } + + if (bi->oper_only && !source.HasPriv("botserv/administration")) + { + source.Reply(ACCESS_DENIED); + return; + } + + if (ci->bi == bi) + { + source.Reply(_("Bot \002%s\002 is already assigned to channel \002%s\002."), ci->bi->nick.c_str(), chan.c_str()); + return; + } + + bool override = !access.HasPriv("ASSIGN"); + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "for " << bi->nick; + + bi->Assign(source.GetUser(), ci); + source.Reply(_("Bot \002%s\002 has been assigned to %s."), bi->nick.c_str(), ci->name.c_str()); + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Assigns the specified bot to a channel. You\n" + "can then configure the bot for the channel so it fits\n" + "your needs.")); + return true; + } +}; + +class CommandBSUnassign final + : public Command +{ +public: + CommandBSUnassign(Module *creator) : Command(creator, "botserv/unassign", 1, 1) + { + this->SetDesc(_("Unassigns a bot from a channel")); + this->SetSyntax(_("\037channel\037")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + if (Anope::ReadOnly) + { + source.Reply(_("Sorry, bot assignment is temporarily disabled.")); + return; + } + + 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("ASSIGN")) + { + source.Reply(ACCESS_DENIED); + return; + } + + if (!ci->bi) + { + source.Reply(BOT_NOT_ASSIGNED); + return; + } + + if (ci->HasExt("PERSIST") && !ModeManager::FindChannelModeByName("PERM")) + { + source.Reply(_("You cannot unassign bots while persist is set on the channel.")); + return; + } + + bool override = !access.HasPriv("ASSIGN"); + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "for " << ci->bi->nick; + + ci->bi->UnAssign(source.GetUser(), ci); + source.Reply(_("There is no bot assigned to %s anymore."), ci->name.c_str()); + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Unassigns a bot from a channel. When you use this command,\n" + "the bot won't join the channel anymore. However, bot\n" + "configuration for the channel is kept, so you will always\n" + "be able to reassign a bot later without having to reconfigure\n" + "it entirely.")); + return true; + } +}; + +class CommandBSSetNoBot final + : public Command +{ +public: + CommandBSSetNoBot(Module *creator, const Anope::string &sname = "botserv/set/nobot") : Command(creator, sname, 2, 2) + { + this->SetDesc(_("Prevent a bot from being assigned to a channel")); + this->SetSyntax(_("\037channel\037 {\037ON|OFF\037}")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + ChannelInfo *ci = ChannelInfo::Find(params[0]); + const Anope::string &value = params[1]; + + if (Anope::ReadOnly) + { + source.Reply(_("Sorry, bot modification is temporarily disabled.")); + return; + } + + if (ci == NULL) + { + source.Reply(CHAN_X_NOT_REGISTERED, params[0].c_str()); + return; + } + + if (value.equals_ci("ON")) + { + Log(LOG_ADMIN, source, this, ci) << "to enable nobot"; + + ci->Extend("BS_NOBOT"); + if (ci->bi) + ci->bi->UnAssign(source.GetUser(), ci); + source.Reply(_("No-bot mode is now \002on\002 on channel %s."), ci->name.c_str()); + } + else if (value.equals_ci("OFF")) + { + Log(LOG_ADMIN, source, this, ci) << "to disable nobot"; + + ci->Shrink("BS_NOBOT"); + source.Reply(_("No-bot mode is now \002off\002 on channel %s."), ci->name.c_str()); + } + else + this->OnSyntaxError(source, source.command); + } + + bool OnHelp(CommandSource &source, const Anope::string &) override + { + this->SendSyntax(source); + source.Reply(_(" \n" + "This option makes a channel unassignable. If a bot\n" + "is already assigned to the channel, it is unassigned\n" + "automatically when you enable it.")); + return true; + } +}; + +class BSAssign final + : public Module +{ + ExtensibleItem nobot; + + CommandBSAssign commandbsassign; + CommandBSUnassign commandbsunassign; + CommandBSSetNoBot commandbssetnobot; + +public: + BSAssign(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), + nobot(this, "BS_NOBOT"), + commandbsassign(this), commandbsunassign(this), commandbssetnobot(this) + { + } + + void OnInvite(User *source, Channel *c, User *targ) override + { + BotInfo *bi; + if (Anope::ReadOnly || !c->ci || targ->server != Me || !(bi = dynamic_cast(targ))) + return; + + AccessGroup access = c->ci->AccessFor(source); + if (nobot.HasExt(c->ci) || (!access.HasPriv("ASSIGN") && !source->HasPriv("botserv/administration"))) + { + targ->SendMessage(bi, ACCESS_DENIED); + return; + } + + if (bi->oper_only && !source->HasPriv("botserv/administration")) + { + targ->SendMessage(bi, ACCESS_DENIED); + return; + } + + if (c->ci->bi == bi) + { + targ->SendMessage(bi, _("Bot \002%s\002 is already assigned to channel \002%s\002."), c->ci->bi->nick.c_str(), c->name.c_str()); + return; + } + + bi->Assign(source, c->ci); + targ->SendMessage(bi, _("Bot \002%s\002 has been assigned to %s."), bi->nick.c_str(), c->name.c_str()); + } + + void OnBotInfo(CommandSource &source, BotInfo *bi, ChannelInfo *ci, InfoFormatter &info) override + { + if (nobot.HasExt(ci)) + info.AddOption(_("No bot")); + } +}; + +MODULE_INIT(BSAssign) diff --git a/modules/botserv/bs_autoassign.cpp b/modules/botserv/bs_autoassign.cpp new file mode 100644 index 000000000..a716525f9 --- /dev/null +++ b/modules/botserv/bs_autoassign.cpp @@ -0,0 +1,37 @@ +/* + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + */ + +#include "module.h" + +class BSAutoAssign final + : public Module +{ +public: + BSAutoAssign(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR) + { + } + + void OnChanRegistered(ChannelInfo *ci) override + { + const Anope::string &bot = Config->GetModule(this)->Get("bot"); + if (bot.empty()) + return; + + BotInfo *bi = BotInfo::Find(bot, true); + if (bi == NULL) + { + Log(this) << "bs_autoassign is configured to assign bot " << bot << ", but it does not exist?"; + return; + } + + bi->Assign(NULL, ci); + } +}; + +MODULE_INIT(BSAutoAssign) diff --git a/modules/botserv/bs_badwords.cpp b/modules/botserv/bs_badwords.cpp new file mode 100644 index 000000000..e513170e7 --- /dev/null +++ b/modules/botserv/bs_badwords.cpp @@ -0,0 +1,476 @@ +/* BotServ core functions + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#include "module.h" +#include "modules/bs_badwords.h" + +struct BadWordImpl final + : BadWord + , Serializable +{ + BadWordImpl() : Serializable("BadWord") { } + ~BadWordImpl() override; + + void Serialize(Serialize::Data &data) const override + { + data["ci"] << this->chan; + data["word"] << this->word; + data.SetType("type", Serialize::Data::DT_INT); data["type"] << this->type; + } + + static Serializable* Unserialize(Serializable *obj, Serialize::Data &); +}; + +struct BadWordsImpl final + : BadWords +{ + Serialize::Reference ci; + typedef std::vector list; + Serialize::Checker badwords; + + BadWordsImpl(Extensible *obj) : ci(anope_dynamic_static_cast(obj)), badwords("BadWord") { } + + ~BadWordsImpl() override; + + BadWord* AddBadWord(const Anope::string &word, BadWordType type) override + { + auto *bw = new BadWordImpl(); + bw->chan = ci->name; + bw->word = word; + bw->type = type; + + this->badwords->push_back(bw); + + FOREACH_MOD(OnBadWordAdd, (ci, bw)); + + return bw; + } + + BadWord* GetBadWord(unsigned index) const override + { + if (this->badwords->empty() || index >= this->badwords->size()) + return NULL; + + BadWordImpl *bw = (*this->badwords)[index]; + bw->QueueUpdate(); + return bw; + } + + unsigned GetBadWordCount() const override + { + return this->badwords->size(); + } + + void EraseBadWord(unsigned index) override + { + if (this->badwords->empty() || index >= this->badwords->size()) + return; + + FOREACH_MOD(OnBadWordDel, (ci, (*this->badwords)[index])); + + delete this->badwords->at(index); + } + + void ClearBadWords() override + { + while (!this->badwords->empty()) + delete this->badwords->back(); + } + + void Check() override + { + if (this->badwords->empty()) + ci->Shrink("badwords"); + } +}; + +BadWordsImpl::~BadWordsImpl() +{ + for (list::iterator it = badwords->begin(); it != badwords->end();) + { + BadWord *bw = *it; + ++it; + delete bw; + } +} + +BadWordImpl::~BadWordImpl() +{ + ChannelInfo *ci = ChannelInfo::Find(chan); + if (ci) + { + BadWordsImpl *badwords = ci->GetExt("badwords"); + if (badwords) + { + BadWordsImpl::list::iterator it = std::find(badwords->badwords->begin(), badwords->badwords->end(), this); + if (it != badwords->badwords->end()) + badwords->badwords->erase(it); + } + } +} + +Serializable* BadWordImpl::Unserialize(Serializable *obj, Serialize::Data &data) +{ + Anope::string sci, sword; + + data["ci"] >> sci; + data["word"] >> sword; + + ChannelInfo *ci = ChannelInfo::Find(sci); + if (!ci) + return NULL; + + unsigned int n; + data["type"] >> n; + + BadWordImpl *bw; + if (obj) + bw = anope_dynamic_static_cast(obj); + else + bw = new BadWordImpl(); + bw->chan = sci; + bw->word = sword; + bw->type = static_cast(n); + + BadWordsImpl *bws = ci->Require("badwords"); + if (!obj) + bws->badwords->push_back(bw); + + return bw; +} + +class BadwordsDelCallback final + : public NumberList +{ + CommandSource &source; + ChannelInfo *ci; + BadWords *bw; + Command *c; + unsigned deleted = 0; + bool override = false; +public: + BadwordsDelCallback(CommandSource &_source, ChannelInfo *_ci, Command *_c, const Anope::string &list) : NumberList(list, true), source(_source), ci(_ci), c(_c) + { + if (!source.AccessFor(ci).HasPriv("BADWORDS") && source.HasPriv("botserv/administration")) + this->override = true; + bw = ci->Require("badwords"); + } + + ~BadwordsDelCallback() override + { + if (!deleted) + source.Reply(_("No matching entries on %s bad words list."), ci->name.c_str()); + else if (deleted == 1) + source.Reply(_("Deleted 1 entry from %s bad words list."), ci->name.c_str()); + else + source.Reply(_("Deleted %d entries from %s bad words list."), deleted, ci->name.c_str()); + } + + void HandleNumber(unsigned Number) override + { + if (!bw || !Number || Number > bw->GetBadWordCount()) + return; + + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, c, ci) << "DEL " << bw->GetBadWord(Number - 1)->word; + ++deleted; + bw->EraseBadWord(Number - 1); + } +}; + +class CommandBSBadwords final + : public Command +{ +private: + void DoList(CommandSource &source, ChannelInfo *ci, const Anope::string &word) + { + bool override = !source.AccessFor(ci).HasPriv("BADWORDS"); + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "LIST"; + ListFormatter list(source.GetAccount()); + BadWords *bw = ci->GetExt("badwords"); + + list.AddColumn(_("Number")).AddColumn(_("Word")).AddColumn(_("Type")); + + if (!bw || !bw->GetBadWordCount()) + { + source.Reply(_("%s bad words list is empty."), ci->name.c_str()); + return; + } + else if (!word.empty() && word.find_first_not_of("1234567890,-") == Anope::string::npos) + { + class BadwordsListCallback final + : public NumberList + { + ListFormatter &list; + BadWords *bw; + public: + BadwordsListCallback(ListFormatter &_list, BadWords *_bw, const Anope::string &numlist) : NumberList(numlist, false), list(_list), bw(_bw) + { + } + + void HandleNumber(unsigned Number) override + { + if (!Number || Number > bw->GetBadWordCount()) + return; + + const BadWord *b = bw->GetBadWord(Number - 1); + ListFormatter::ListEntry entry; + entry["Number"] = stringify(Number); + entry["Word"] = b->word; + entry["Type"] = b->type == BW_SINGLE ? "(SINGLE)" : (b->type == BW_START ? "(START)" : (b->type == BW_END ? "(END)" : "")); + this->list.AddEntry(entry); + } + } + nl_list(list, bw, word); + nl_list.Process(); + } + else + { + for (unsigned i = 0, end = bw->GetBadWordCount(); i < end; ++i) + { + const BadWord *b = bw->GetBadWord(i); + + if (!word.empty() && !Anope::Match(b->word, word)) + continue; + + ListFormatter::ListEntry entry; + entry["Number"] = stringify(i + 1); + entry["Word"] = b->word; + entry["Type"] = b->type == BW_SINGLE ? "(SINGLE)" : (b->type == BW_START ? "(START)" : (b->type == BW_END ? "(END)" : "")); + list.AddEntry(entry); + } + } + + if (list.IsEmpty()) + source.Reply(_("No matching entries on %s bad words list."), ci->name.c_str()); + else + { + std::vector replies; + list.Process(replies); + + source.Reply(_("Bad words list for %s:"), ci->name.c_str()); + + for (const auto &reply : replies) + source.Reply(reply); + + source.Reply(_("End of bad words list.")); + } + } + + void DoAdd(CommandSource &source, ChannelInfo *ci, const Anope::string &word) + { + size_t pos = word.rfind(' '); + BadWordType bwtype = BW_ANY; + Anope::string realword = word; + BadWords *badwords = ci->Require("badwords"); + + if (pos != Anope::string::npos) + { + Anope::string opt = word.substr(pos + 1); + if (!opt.empty()) + { + if (opt.equals_ci("SINGLE")) + bwtype = BW_SINGLE; + else if (opt.equals_ci("START")) + bwtype = BW_START; + else if (opt.equals_ci("END")) + bwtype = BW_END; + } + realword = word.substr(0, pos); + } + + unsigned badwordsmax = Config->GetModule(this->module)->Get("badwordsmax"); + if (badwords->GetBadWordCount() >= badwordsmax) + { + source.Reply(_("Sorry, you can only have %d bad words entries on a channel."), badwordsmax); + return; + } + + bool casesensitive = Config->GetModule(this->module)->Get("casesensitive"); + + for (unsigned i = 0, end = badwords->GetBadWordCount(); i < end; ++i) + { + const BadWord *bw = badwords->GetBadWord(i); + + if ((casesensitive && realword.equals_cs(bw->word)) || (!casesensitive && realword.equals_ci(bw->word))) + { + source.Reply(_("\002%s\002 already exists in %s bad words list."), bw->word.c_str(), ci->name.c_str()); + return; + } + } + + bool override = !source.AccessFor(ci).HasPriv("BADWORDS"); + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "ADD " << realword; + badwords->AddBadWord(realword, bwtype); + + source.Reply(_("\002%s\002 added to %s bad words list."), realword.c_str(), ci->name.c_str()); + } + + void DoDelete(CommandSource &source, ChannelInfo *ci, const Anope::string &word) + { + BadWords *badwords = ci->GetExt("badwords"); + + if (!badwords || !badwords->GetBadWordCount()) + { + source.Reply(_("%s bad words list is empty."), ci->name.c_str()); + return; + } + + /* Special case: is it a number/list? Only do search if it isn't. */ + if (!word.empty() && isdigit(word[0]) && word.find_first_not_of("1234567890,-") == Anope::string::npos) + { + BadwordsDelCallback list(source, ci, this, word); + list.Process(); + } + else + { + unsigned i, end; + const BadWord *badword; + + for (i = 0, end = badwords->GetBadWordCount(); i < end; ++i) + { + badword = badwords->GetBadWord(i); + + if (word.equals_ci(badword->word)) + break; + } + + if (i == end) + { + source.Reply(_("\002%s\002 not found on %s bad words list."), word.c_str(), ci->name.c_str()); + return; + } + + bool override = !source.AccessFor(ci).HasPriv("BADWORDS"); + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "DEL " << badword->word; + + source.Reply(_("\002%s\002 deleted from %s bad words list."), badword->word.c_str(), ci->name.c_str()); + + badwords->EraseBadWord(i); + } + + badwords->Check(); + } + + void DoClear(CommandSource &source, ChannelInfo *ci) + { + bool override = !source.AccessFor(ci).HasPriv("BADWORDS"); + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "CLEAR"; + + BadWords *badwords = ci->GetExt("badwords"); + if (badwords) + badwords->ClearBadWords(); + source.Reply(_("Bad words list is now empty.")); + } + +public: + CommandBSBadwords(Module *creator) : Command(creator, "botserv/badwords", 2, 3) + { + this->SetDesc(_("Maintains the bad words list")); + this->SetSyntax(_("\037channel\037 ADD \037word\037 [\037SINGLE\037 | \037START\037 | \037END\037]")); + this->SetSyntax(_("\037channel\037 DEL {\037word\037 | \037entry-num\037 | \037list\037}")); + this->SetSyntax(_("\037channel\037 LIST [\037mask\037 | \037list\037]")); + this->SetSyntax(_("\037channel\037 CLEAR")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + const Anope::string &cmd = params[1]; + const Anope::string &word = params.size() > 2 ? params[2] : ""; + bool need_args = cmd.equals_ci("LIST") || cmd.equals_ci("CLEAR"); + + if (!need_args && word.empty()) + { + this->OnSyntaxError(source, cmd); + return; + } + + ChannelInfo *ci = ChannelInfo::Find(params[0]); + if (ci == NULL) + { + source.Reply(CHAN_X_NOT_REGISTERED, params[0].c_str()); + return; + } + + if (!source.AccessFor(ci).HasPriv("BADWORDS") && !source.HasPriv("botserv/administration")) + { + source.Reply(ACCESS_DENIED); + return; + } + + if (Anope::ReadOnly) + { + source.Reply(_("Sorry, bad words list modification is temporarily disabled.")); + return; + } + + if (cmd.equals_ci("ADD")) + return this->DoAdd(source, ci, word); + else if (cmd.equals_ci("DEL")) + return this->DoDelete(source, ci, word); + else if (cmd.equals_ci("LIST")) + return this->DoList(source, ci, word); + else if (cmd.equals_ci("CLEAR")) + return this->DoClear(source, ci); + else + this->OnSyntaxError(source, ""); + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Maintains the \002bad words list\002 for a channel. The bad\n" + "words list determines which words are to be kicked\n" + "when the bad words kicker is enabled. For more information,\n" + "type \002%s%s HELP KICK %s\002.\n" + " \n" + "The \002ADD\002 command adds the given word to the\n" + "bad words list. If SINGLE is specified, a kick will be\n" + "done only if a user says the entire word. If START is\n" + "specified, a kick will be done if a user says a word\n" + "that starts with \037word\037. If END is specified, a kick\n" + "will be done if a user says a word that ends with\n" + "\037word\037. If you don't specify anything, a kick will\n" + "be issued every time \037word\037 is said by a user.\n" + " \n"), Config->StrictPrivmsg.c_str(), source.service->nick.c_str(), source.command.c_str()); + source.Reply(_("The \002DEL\002 command removes the given word from the\n" + "bad words list. If a list of entry numbers is given, those\n" + "entries are deleted. (See the example for LIST below.)\n" + " \n" + "The \002LIST\002 command displays the bad words list. If\n" + "a wildcard mask is given, only those entries matching the\n" + "mask are displayed. If a list of entry numbers is given,\n" + "only those entries are shown; for example:\n" + " \002#channel LIST 2-5,7-9\002\n" + " Lists bad words entries numbered 2 through 5 and\n" + " 7 through 9.\n" + " \n" + "The \002CLEAR\002 command clears all entries from the\n" + "bad words list.")); + return true; + } +}; + +class BSBadwords final + : public Module +{ + CommandBSBadwords commandbsbadwords; + ExtensibleItem badwords; + Serialize::Type badword_type; + +public: + BSBadwords(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), + commandbsbadwords(this), badwords(this, "badwords"), badword_type("BadWord", BadWordImpl::Unserialize) + { + } +}; + +MODULE_INIT(BSBadwords) diff --git a/modules/botserv/bs_bot.cpp b/modules/botserv/bs_bot.cpp new file mode 100644 index 000000000..170e1073a --- /dev/null +++ b/modules/botserv/bs_bot.cpp @@ -0,0 +1,387 @@ +/* BotServ core functions + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#include "module.h" + +class CommandBSBot final + : public Command +{ +private: + void DoAdd(CommandSource &source, const std::vector ¶ms) + { + const Anope::string &nick = params[1]; + const Anope::string &user = params[2]; + const Anope::string &host = params[3]; + const Anope::string &real = params[4]; + + if (BotInfo::Find(nick, true)) + { + source.Reply(_("Bot \002%s\002 already exists."), nick.c_str()); + return; + } + + Configuration::Block *networkinfo = Config->GetBlock("networkinfo"); + + if (nick.length() > networkinfo->Get("nicklen")) + { + source.Reply(_("Bot nicks may only be %d characters long."), networkinfo->Get("nicklen")); + return; + } + + if (user.length() > networkinfo->Get("userlen")) + { + source.Reply(_("Bot idents may only be %d characters long."), networkinfo->Get("userlen")); + return; + } + + if (host.length() > networkinfo->Get("hostlen")) + { + source.Reply(_("Bot hosts may only be %d characters long."), networkinfo->Get("hostlen")); + return; + } + + if (!IRCD->IsNickValid(nick)) + { + source.Reply(_("Bot nicks may only contain valid nick characters.")); + return; + } + + if (!IRCD->IsIdentValid(user)) + { + source.Reply(_("Bot idents may only contain valid ident characters.")); + return; + } + + if (!IRCD->IsHostValid(host)) + { + source.Reply(_("Bot hosts may only contain valid host characters.")); + return; + } + + /* We check whether the nick is registered, and inform the user + * if so. You need to drop the nick manually before you can use + * it as a bot nick from now on -GD + */ + if (NickAlias::Find(nick)) + { + source.Reply(NICK_ALREADY_REGISTERED, nick.c_str()); + return; + } + + User *u = User::Find(nick, true); + if (u) + { + source.Reply(_("Nick \002%s\002 is currently in use."), u->nick.c_str()); + return; + } + + auto *bi = new BotInfo(nick, user, host, real); + + Log(LOG_ADMIN, source, this) << "ADD " << bi->GetMask() << " " << bi->realname; + + source.Reply(_("%s!%s@%s (%s) added to the bot list."), bi->nick.c_str(), bi->GetIdent().c_str(), bi->host.c_str(), bi->realname.c_str()); + + FOREACH_MOD(OnBotCreate, (bi)); + return; + } + + void DoChange(CommandSource &source, const std::vector ¶ms) + { + const Anope::string &oldnick = params[1]; + const Anope::string &nick = params.size() > 2 ? params[2] : ""; + const Anope::string &user = params.size() > 3 ? params[3] : ""; + const Anope::string &host = params.size() > 4 ? params[4] : ""; + const Anope::string &real = params.size() > 5 ? params[5] : ""; + + if (oldnick.empty() || nick.empty()) + { + this->OnSyntaxError(source, "CHANGE"); + return; + } + + BotInfo *bi = BotInfo::Find(oldnick, true); + if (!bi) + { + source.Reply(BOT_DOES_NOT_EXIST, oldnick.c_str()); + return; + } + + if (bi->conf) + { + source.Reply(_("Bot %s is not changeable."), bi->nick.c_str()); + return; + } + + Configuration::Block *networkinfo = Config->GetBlock("networkinfo"); + + if (nick.length() > networkinfo->Get("nicklen")) + { + source.Reply(_("Bot nicks may only be %d characters long."), networkinfo->Get("nicklen")); + return; + } + + if (user.length() > networkinfo->Get("userlen")) + { + source.Reply(_("Bot idents may only be %d characters long."), networkinfo->Get("userlen")); + return; + } + + if (host.length() > networkinfo->Get("hostlen")) + { + source.Reply(_("Bot hosts may only be %d characters long."), networkinfo->Get("hostlen")); + return; + } + + /* Checks whether there *are* changes. + * Case sensitive because we may want to change just the case. + * And we must finally check that the nick is not already + * taken by another bot. + */ + if (nick.equals_cs(bi->nick) && (!user.empty() ? user.equals_cs(bi->GetIdent()) : 1) && (!host.empty() ? host.equals_cs(bi->host) : 1) && (!real.empty() ? real.equals_cs(bi->realname) : 1)) + { + source.Reply(_("The old information is the same as the new information specified.")); + return; + } + + if (!IRCD->IsNickValid(nick)) + { + source.Reply(_("Bot nicks may only contain valid nick characters.")); + return; + } + + if (!user.empty() && !IRCD->IsIdentValid(user)) + { + source.Reply(_("Bot idents may only contain valid ident characters.")); + return; + } + + if (!host.empty() && !IRCD->IsHostValid(host)) + { + source.Reply(_("Bot hosts may only contain valid host characters.")); + return; + } + + if (!nick.equals_ci(bi->nick)) + { + if (BotInfo::Find(nick, true)) + { + source.Reply(_("Bot \002%s\002 already exists."), nick.c_str()); + return; + } + + if (User::Find(nick, true)) + { + source.Reply(_("Nick \002%s\002 is currently in use."), nick.c_str()); + return; + } + } + + if (!nick.equals_ci(bi->nick)) + { + /* We check whether the nick is registered, and inform the user + * if so. You need to drop the nick manually before you can use + * it as a bot nick from now on -GD + */ + if (NickAlias::Find(nick)) + { + source.Reply(NICK_ALREADY_REGISTERED, nick.c_str()); + return; + } + + /* The new nick is really different, so we remove the Q line for the old nick. */ + XLine x_del(bi->nick); + IRCD->SendSQLineDel(&x_del); + + /* Add a Q line for the new nick */ + XLine x(nick, "Reserved for services"); + IRCD->SendSQLine(NULL, &x); + } + + if (!user.empty()) + { + IRCD->SendQuit(bi, "Quit: Be right back"); + bi->introduced = false; + } + else + IRCD->SendNickChange(bi, nick); + + if (!nick.equals_cs(bi->nick)) + bi->SetNewNick(nick); + + if (!user.empty() && !user.equals_cs(bi->GetIdent())) + bi->SetIdent(user); + if (!host.empty() && !host.equals_cs(bi->host)) + bi->host = host; + if (!real.empty() && !real.equals_cs(bi->realname)) + bi->realname = real; + + if (!user.empty()) + bi->OnKill(); + + source.Reply(_("Bot \002%s\002 has been changed to %s!%s@%s (%s)."), oldnick.c_str(), bi->nick.c_str(), bi->GetIdent().c_str(), bi->host.c_str(), bi->realname.c_str()); + Log(LOG_ADMIN, source, this) << "CHANGE " << oldnick << " to " << bi->GetMask() << " " << bi->realname; + + FOREACH_MOD(OnBotChange, (bi)); + return; + } + + void DoDel(CommandSource &source, const std::vector ¶ms) + { + const Anope::string &nick = params[1]; + + if (nick.empty()) + { + this->OnSyntaxError(source, "DEL"); + return; + } + + BotInfo *bi = BotInfo::Find(nick, true); + if (!bi) + { + source.Reply(BOT_DOES_NOT_EXIST, nick.c_str()); + return; + } + + if (bi->conf) + { + source.Reply(_("Bot %s is not deletable."), bi->nick.c_str()); + return; + } + + FOREACH_MOD(OnBotDelete, (bi)); + + Log(LOG_ADMIN, source, this) << "DEL " << bi->nick; + + source.Reply(_("Bot \002%s\002 has been deleted."), nick.c_str()); + delete bi; + return; + } +public: + CommandBSBot(Module *creator) : Command(creator, "botserv/bot", 1, 6) + { + this->SetDesc(_("Maintains network bot list")); + this->SetSyntax(_("\002ADD \037nick\037 \037user\037 \037host\037 \037real\037\002")); + this->SetSyntax(_("\002CHANGE \037oldnick\037 \037newnick\037 [\037user\037 [\037host\037 [\037real\037]]]\002")); + this->SetSyntax(_("\002DEL \037nick\037\002")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + const Anope::string &cmd = params[0]; + + if (Anope::ReadOnly) + { + source.Reply(_("Sorry, bot modification is temporarily disabled.")); + return; + } + + if (cmd.equals_ci("ADD")) + { + // ADD nick user host real - 5 + if (!source.HasCommand("botserv/bot/add")) + { + source.Reply(ACCESS_DENIED); + return; + } + + if (params.size() < 5) + { + this->OnSyntaxError(source, "ADD"); + return; + } + + std::vector tempparams = params; + // ADD takes less params than CHANGE, so we need to take 6 if given and append it with a space to 5. + if (tempparams.size() >= 6) + tempparams[4] = tempparams[4] + " " + tempparams[5]; + + return this->DoAdd(source, tempparams); + } + else if (cmd.equals_ci("CHANGE")) + { + // CHANGE oldn newn user host real - 6 + // but only oldn and newn are required + if (!source.HasCommand("botserv/bot/change")) + { + source.Reply(ACCESS_DENIED); + return; + } + + if (params.size() < 3) + { + this->OnSyntaxError(source, "CHANGE"); + return; + } + + return this->DoChange(source, params); + } + else if (cmd.equals_ci("DEL")) + { + // DEL nick + if (!source.HasCommand("botserv/bot/del")) + { + source.Reply(ACCESS_DENIED); + return; + } + + if (params.size() < 1) + { + this->OnSyntaxError(source, "DEL"); + return; + } + + return this->DoDel(source, params); + } + else + this->OnSyntaxError(source, ""); + + return; + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Allows Services Operators to create, modify, and delete\n" + "bots that users will be able to use on their own\n" + "channels.\n" + " \n" + "\002BOT ADD\002 adds a bot with the given nickname, username,\n" + "hostname and realname. Since no integrity checks are done\n" + "for these settings, be really careful.\n" + " \n" + "\002BOT CHANGE\002 allows you to change the nickname, username, hostname\n" + "or realname of a bot without deleting it (and\n" + "all the data associated with it).\n" + " \n" + "\002BOT DEL\002 removes the given bot from the bot list.\n" + " \n" + "\002Note\002: You cannot create a bot with a nick that is\n" + "currently registered. If an unregistered user is currently\n" + "using the nick, they will be killed.")); + return true; + } +}; + +class BSBot final + : public Module +{ + CommandBSBot commandbsbot; + +public: + BSBot(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), + commandbsbot(this) + { + + } +}; + +MODULE_INIT(BSBot) diff --git a/modules/botserv/bs_botlist.cpp b/modules/botserv/bs_botlist.cpp new file mode 100644 index 000000000..915117916 --- /dev/null +++ b/modules/botserv/bs_botlist.cpp @@ -0,0 +1,82 @@ +/* BotServ core functions + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#include "module.h" + +class CommandBSBotList final + : public Command +{ +public: + CommandBSBotList(Module *creator) : Command(creator, "botserv/botlist", 0, 0) + { + this->SetDesc(_("Lists available bots")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + unsigned count = 0; + ListFormatter list(source.GetAccount()); + + list.AddColumn(_("Nick")).AddColumn(_("Mask")); + + for (const auto &[_, bi] : *BotListByNick) + { + if (source.HasPriv("botserv/administration") || !bi->oper_only) + { + ++count; + ListFormatter::ListEntry entry; + entry["Nick"] = (bi->oper_only ? "* " : "") + bi->nick; + entry["Mask"] = bi->GetIdent() + "@" + bi->host; + list.AddEntry(entry); + } + } + + std::vector replies; + list.Process(replies); + + if (!count) + source.Reply(_("There are no bots available at this time.\n" + "Ask a Services Operator to create one!")); + else + { + source.Reply(_("Bot list:")); + + for (const auto &reply : replies) + source.Reply(reply); + + source.Reply(_("%d bots available."), count); + } + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Lists all available bots on this network.\n" + "Bots prefixed by a * are reserved for IRC Operators.")); + return true; + } +}; + +class BSBotList final + : public Module +{ + CommandBSBotList commandbsbotlist; + +public: + BSBotList(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), + commandbsbotlist(this) + { + + } +}; + +MODULE_INIT(BSBotList) diff --git a/modules/botserv/bs_control.cpp b/modules/botserv/bs_control.cpp new file mode 100644 index 000000000..32101be6e --- /dev/null +++ b/modules/botserv/bs_control.cpp @@ -0,0 +1,149 @@ +/* BotServ core functions + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#include "module.h" + +class CommandBSSay final + : public Command +{ +public: + CommandBSSay(Module *creator) : Command(creator, "botserv/say", 2, 2) + { + this->SetDesc(_("Makes the bot say the specified text on the specified channel")); + this->SetSyntax(_("\037channel\037 \037text\037")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + const Anope::string &text = params[1]; + + ChannelInfo *ci = ChannelInfo::Find(params[0]); + if (ci == NULL) + { + source.Reply(CHAN_X_NOT_REGISTERED, params[0].c_str()); + return; + } + + if (!source.AccessFor(ci).HasPriv("SAY") && !source.HasPriv("botserv/administration")) + { + source.Reply(ACCESS_DENIED); + return; + } + + if (!ci->bi) + { + source.Reply(BOT_NOT_ASSIGNED); + return; + } + + if (!ci->c || !ci->c->FindUser(ci->bi)) + { + source.Reply(BOT_NOT_ON_CHANNEL, ci->name.c_str()); + return; + } + + if (text[0] == '\001') + { + this->OnSyntaxError(source, ""); + return; + } + + IRCD->SendPrivmsg(*ci->bi, ci->name, "%s", text.c_str()); + ci->bi->lastmsg = Anope::CurTime; + + bool override = !source.AccessFor(ci).HasPriv("SAY"); + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to say: " << text; + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Makes the bot say the specified text on the specified channel.")); + return true; + } +}; + +class CommandBSAct final + : public Command +{ +public: + CommandBSAct(Module *creator) : Command(creator, "botserv/act", 2, 2) + { + this->SetDesc(_("Makes the bot do the equivalent of a \"/me\" command")); + this->SetSyntax(_("\037channel\037 \037text\037")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + Anope::string message = params[1]; + + ChannelInfo *ci = ChannelInfo::Find(params[0]); + if (ci == NULL) + { + source.Reply(CHAN_X_NOT_REGISTERED, params[0].c_str()); + return; + } + + if (!source.AccessFor(ci).HasPriv("SAY") && !source.HasPriv("botserv/administration")) + { + source.Reply(ACCESS_DENIED); + return; + } + + if (!ci->bi) + { + source.Reply(BOT_NOT_ASSIGNED); + return; + } + + if (!ci->c || !ci->c->FindUser(ci->bi)) + { + source.Reply(BOT_NOT_ON_CHANNEL, ci->name.c_str()); + return; + } + + message = message.replace_all_cs("\1", ""); + if (message.empty()) + return; + + IRCD->SendAction(*ci->bi, ci->name, "%s", message.c_str()); + ci->bi->lastmsg = Anope::CurTime; + + bool override = !source.AccessFor(ci).HasPriv("SAY"); + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to say: " << message; + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Makes the bot do the equivalent of a \"/me\" command\n" + "on the specified channel using the specified text.")); + return true; + } +}; + +class BSControl final + : public Module +{ + CommandBSSay commandbssay; + CommandBSAct commandbsact; + +public: + BSControl(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), + commandbssay(this), commandbsact(this) + { + + } +}; + +MODULE_INIT(BSControl) diff --git a/modules/botserv/bs_info.cpp b/modules/botserv/bs_info.cpp new file mode 100644 index 000000000..e42ad23b8 --- /dev/null +++ b/modules/botserv/bs_info.cpp @@ -0,0 +1,134 @@ +/* BotServ core functions + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#include "module.h" + +class CommandBSInfo final + : public Command +{ +private: + void send_bot_channels(std::vector &buffers, const BotInfo *bi) + { + Anope::string buf; + for (const auto &[_, ci] : *RegisteredChannelList) + { + if (ci->bi == bi) + { + buf += " " + ci->name + " "; + if (buf.length() > 300) + { + buffers.push_back(buf); + buf.clear(); + } + } + } + if (!buf.empty()) + buffers.push_back(buf); + } + +public: + CommandBSInfo(Module *creator) : Command(creator, "botserv/info", 1, 1) + { + this->SetSyntax(_("{\037channel\037 | \037nickname\037}")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + const Anope::string &query = params[0]; + + BotInfo *bi = BotInfo::Find(query, true); + ChannelInfo *ci = ChannelInfo::Find(query); + InfoFormatter info(source.nc); + + if (bi) + { + source.Reply(_("Information for bot \002%s\002:"), bi->nick.c_str()); + info[_("Mask")] = bi->GetIdent() + "@" + bi->host; + info[_("Real name")] = bi->realname; + info[_("Created")] = Anope::strftime(bi->created, source.GetAccount()); + info[_("Options")] = bi->oper_only ? _("Private") : _("None"); + info[_("Used on")] = stringify(bi->GetChannelCount()) + " channel(s)"; + + FOREACH_MOD(OnBotInfo, (source, bi, ci, info)); + + std::vector replies; + info.Process(replies); + + for (const auto &reply : replies) + source.Reply(reply); + + if (source.HasPriv("botserv/administration")) + { + std::vector buf; + this->send_bot_channels(buf, bi); + for (const auto &line : buf) + source.Reply(line); + } + + } + else if (ci) + { + if (!source.AccessFor(ci).HasPriv("INFO") && !source.HasPriv("botserv/administration")) + { + source.Reply(ACCESS_DENIED); + return; + } + + source.Reply(CHAN_INFO_HEADER, ci->name.c_str()); + info[_("Bot nick")] = ci->bi ? ci->bi->nick : _("not assigned yet"); + + Anope::string enabled = Language::Translate(source.nc, _("Enabled")); + Anope::string disabled = Language::Translate(source.nc, _("Disabled")); + + FOREACH_MOD(OnBotInfo, (source, bi, ci, info)); + + std::vector replies; + info.Process(replies); + + for (const auto &reply : replies) + source.Reply(reply); + } + else + source.Reply(_("\002%s\002 is not a valid bot or registered channel."), query.c_str()); + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Allows you to see %s information about a channel or a bot.\n" + "If the parameter is a channel, then you'll get information\n" + "such as enabled kickers. If the parameter is a nick,\n" + "you'll get information about a bot, such as creation\n" + "time or number of channels it is on."), source.service->nick.c_str()); + return true; + } + + const Anope::string GetDesc(CommandSource &source) const override + { + return Anope::printf(Language::Translate(source.GetAccount(), _("Allows you to see %s information about a channel or a bot")), source.service->nick.c_str()); + } +}; + +class BSInfo final + : public Module +{ + CommandBSInfo commandbsinfo; + +public: + BSInfo(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), + commandbsinfo(this) + { + + } +}; + +MODULE_INIT(BSInfo) diff --git a/modules/botserv/bs_kick.cpp b/modules/botserv/bs_kick.cpp new file mode 100644 index 000000000..f6012d07f --- /dev/null +++ b/modules/botserv/bs_kick.cpp @@ -0,0 +1,1491 @@ +/* BotServ core functions + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#include "module.h" +#include "modules/bs_kick.h" +#include "modules/bs_badwords.h" + +static Module *me; + +struct KickerDataImpl final + : 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("kickerdata"); + } + + 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") + return; + + const ChannelInfo *ci = anope_dynamic_static_cast(e); + KickerData *kd = this->Get(ci); + if (kd == NULL) + return; + + data["kickerdata:amsgs"] << kd->amsgs; + data["kickerdata:badwords"] << kd->badwords; + data["kickerdata:bolds"] << kd->bolds; + data["kickerdata:caps"] << kd->caps; + data["kickerdata:colors"] << kd->colors; + data["kickerdata:flood"] << kd->flood; + data["kickerdata:italics"] << kd->italics; + data["kickerdata:repeat"] << kd->repeat; + data["kickerdata:reverses"] << kd->reverses; + data["kickerdata:underlines"] << kd->underlines; + + data.SetType("capsmin", Serialize::Data::DT_INT); data["capsmin"] << kd->capsmin; + data.SetType("capspercent", Serialize::Data::DT_INT); data["capspercent"] << kd->capspercent; + data.SetType("floodlines", Serialize::Data::DT_INT); data["floodlines"] << kd->floodlines; + data.SetType("floodsecs", Serialize::Data::DT_INT); data["floodsecs"] << kd->floodsecs; + data.SetType("repeattimes", Serialize::Data::DT_INT); data["repeattimes"] << kd->repeattimes; + data.SetType("dontkickops", Serialize::Data::DT_INT); data["dontkickops"] << kd->dontkickops; + data.SetType("dontkickvoices", Serialize::Data::DT_INT); data["dontkickvoices"] << kd->dontkickvoices; + for (auto ttbtype : kd->ttb) + data["ttb"] << ttbtype << " "; + } + + void ExtensibleUnserialize(Extensible *e, Serializable *s, Serialize::Data &data) override + { + if (s->GetSerializableType()->GetName() != "ChannelInfo") + return; + + ChannelInfo *ci = anope_dynamic_static_cast(e); + KickerData *kd = ci->Require("kickerdata"); + + data["kickerdata:amsgs"] >> kd->amsgs; + data["kickerdata:badwords"] >> kd->badwords; + data["kickerdata:bolds"] >> kd->bolds; + data["kickerdata:caps"] >> kd->caps; + data["kickerdata:colors"] >> kd->colors; + data["kickerdata:flood"] >> kd->flood; + data["kickerdata:italics"] >> kd->italics; + data["kickerdata:repeat"] >> kd->repeat; + data["kickerdata:reverses"] >> kd->reverses; + data["kickerdata:underlines"] >> kd->underlines; + + data["capsmin"] >> kd->capsmin; + data["capspercent"] >> kd->capspercent; + data["floodlines"] >> kd->floodlines; + data["floodsecs"] >> kd->floodsecs; + data["repeattimes"] >> kd->repeattimes; + data["dontkickops"] >> kd->dontkickops; + data["dontkickvoices"] >> kd->dontkickvoices; + + Anope::string ttb, tok; + data["ttb"] >> ttb; + spacesepstream sep(ttb); + for (int i = 0; sep.GetToken(tok) && i < TTB_SIZE; ++i) + try + { + kd->ttb[i] = convertTo(tok); + } + catch (const ConvertException &) { } + + 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:")); + + 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); + } + } + } + + source.Reply(_("Type \002%s%s HELP %s \037option\037\002 for more information\n" + "on a specific option.\n" + " \n" + "Note: access to this command is controlled by the\n" + "level SET."), Config->StrictPrivmsg.c_str(), source.service->nick.c_str(), 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(_("Sorry, kicker configuration is temporarily disabled.")); + 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, KickerData *kd, bool &val) + { + if (param.equals_ci("ON")) + { + if (!ttb.empty()) + { + int16_t i; + + try + { + i = convertTo(ttb); + if (i < 0) + throw ConvertException(); + } + catch (const ConvertException &) + { + source.Reply(_("\002%s\002 cannot be taken as times to ban."), ttb.c_str()); + return; + } + + kd->ttb[ttb_idx] = i; + } + 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\n" + "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)) + { + KickerData *kd = ci->Require("kickerdata"); + Process(source, ci, params[1], params.size() > 2 ? params[2] : "", 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\n" + "kick users who send the same message to multiple channels\n" + "where %s bots are.\n" + " \n" + "\037ttb\037 is the number of times a user can be kicked\n" + "before they get banned. Don't give ttb to disable\n" + "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)) + { + KickerData *kd = ci->Require("kickerdata"); + Process(source, ci, params[1], params.size() > 2 ? params[2] : "", TTB_BADWORDS, "badwords", 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\n" + "option tells the bot to kick users who say certain words\n" + "on the channels.\n" + "You can define bad words for your channel using the\n" + "\002BADWORDS\002 command. Type \002%s%s HELP BADWORDS\002 for\n" + "more information.\n" + " \n" + "\037ttb\037 is the number of times a user can be kicked\n" + "before it gets banned. Don't give ttb to disable\n" + "the ban system once activated."), Config->StrictPrivmsg.c_str(), source.service->nick.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)) + { + KickerData *kd = ci->Require("kickerdata"); + Process(source, ci, params[1], params.size() > 2 ? params[2] : "", 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\n" + "option tells the bot to kick users who use bolds.\n" + " \n" + "\037ttb\037 is the number of times a user can be kicked\n" + "before it gets banned. Don't give ttb to disable\n" + "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; + + KickerData *kd = ci->Require("kickerdata"); + + 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()) + { + try + { + kd->ttb[TTB_CAPS] = convertTo(ttb); + if (kd->ttb[TTB_CAPS] < 0) + throw ConvertException(); + } + catch (const ConvertException &) + { + kd->ttb[TTB_CAPS] = 0; + source.Reply(_("\002%s\002 cannot be taken as times to ban."), ttb.c_str()); + return; + } + } + else + kd->ttb[TTB_CAPS] = 0; + + kd->capsmin = 10; +