diff options
| author | Sadie Powell <sadie@witchery.services> | 2024-01-24 12:34:03 +0000 |
|---|---|---|
| committer | Sadie Powell <sadie@witchery.services> | 2024-01-24 12:34:34 +0000 |
| commit | 484160eb4ed560eeda97c94a42dd8e31431ab251 (patch) | |
| tree | 03600af16b55fa2f268904d7a42a8b87f86c09f4 /modules/botserv | |
| parent | 7ac1fe58478d58e2480b6919c4abf3a82929169c (diff) | |
Shuffle modules around a bit.
Diffstat (limited to 'modules/botserv')
| -rw-r--r-- | modules/botserv/botserv.cpp | 213 | ||||
| -rw-r--r-- | modules/botserv/bs_assign.cpp | 260 | ||||
| -rw-r--r-- | modules/botserv/bs_autoassign.cpp | 37 | ||||
| -rw-r--r-- | modules/botserv/bs_badwords.cpp | 476 | ||||
| -rw-r--r-- | modules/botserv/bs_bot.cpp | 387 | ||||
| -rw-r--r-- | modules/botserv/bs_botlist.cpp | 82 | ||||
| -rw-r--r-- | modules/botserv/bs_control.cpp | 149 | ||||
| -rw-r--r-- | modules/botserv/bs_info.cpp | 134 | ||||
| -rw-r--r-- | modules/botserv/bs_kick.cpp | 1491 | ||||
| -rw-r--r-- | modules/botserv/bs_set.cpp | 231 |
10 files changed, 3460 insertions, 0 deletions
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<BotInfo> BotServ; + ExtensibleRef<bool> 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<const Anope::string>("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<const Anope::string>("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<unsigned>("minusers")) + { + ChannelStatus status(Config->GetModule(this)->Get<const Anope::string>("botmodes")); + bi->Join(ci->c, &status); + } + } + + void OnJoinChannel(User *user, Channel *c) override + { + if (!Config || !IRCD) + return; + + BotInfo *bi = user->server == Me ? dynamic_cast<BotInfo *>(user) : NULL; + if (bi && Config->GetModule(this)->Get<bool>("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<unsigned>(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<ChannelModeStatus *>(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<unsigned>("minusers") && !c->FindUser(c->ci->bi)) + { + ChannelStatus status(Config->GetModule(this)->Get<const Anope::string>("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<unsigned>("minusers") && c->FindUser(c->ci->bi)) + c->ci->bi->Part(c->ci->c); + } + + EventReturn OnPreHelp(CommandSource &source, const std::vector<Anope::string> ¶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<const Anope::string>("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<Anope::string> ¶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<unsigned>("minusers")); + const Anope::string &fantasycharacters = Config->GetModule("fantasy")->Get<const Anope::string>("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<bool>("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<const Anope::string>("defaults", "greet fantasy")); + for (Anope::string token; sep.GetToken(token);) + ci->Extend<bool>("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<const Anope::string>("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<Anope::string> ¶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<Anope::string> ¶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<Anope::string> ¶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<bool>("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<bool>("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<bool> 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<BotInfo *>(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<const Anope::string>("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<ChannelInfo> ci; + typedef std::vector<BadWordImpl *> list; + Serialize::Checker<list> badwords; + + BadWordsImpl(Extensible *obj) : ci(anope_dynamic_static_cast<ChannelInfo *>(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>("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<BadWordsImpl>("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<BadWordImpl *>(obj); + else + bw = new BadWordImpl(); + bw->chan = sci; + bw->word = sword; + bw->type = static_cast<BadWordType>(n); + + BadWordsImpl *bws = ci->Require<BadWordsImpl>("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>("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>("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<Anope::string> 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>("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<unsigned>("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<bool>("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>("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>("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<Anope::string> ¶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<BadWordsImpl> 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. |
