summaryrefslogtreecommitdiff
path: root/modules/chanserv
diff options
context:
space:
mode:
authorSadie Powell <sadie@witchery.services>2024-01-24 12:34:03 +0000
committerSadie Powell <sadie@witchery.services>2024-01-24 12:34:34 +0000
commit484160eb4ed560eeda97c94a42dd8e31431ab251 (patch)
tree03600af16b55fa2f268904d7a42a8b87f86c09f4 /modules/chanserv
parent7ac1fe58478d58e2480b6919c4abf3a82929169c (diff)
Shuffle modules around a bit.
Diffstat (limited to 'modules/chanserv')
-rw-r--r--modules/chanserv/chanserv.cpp481
-rw-r--r--modules/chanserv/cs_access.cpp939
-rw-r--r--modules/chanserv/cs_akick.cpp585
-rw-r--r--modules/chanserv/cs_ban.cpp254
-rw-r--r--modules/chanserv/cs_clone.cpp262
-rw-r--r--modules/chanserv/cs_drop.cpp97
-rw-r--r--modules/chanserv/cs_enforce.cpp279
-rw-r--r--modules/chanserv/cs_entrymsg.cpp294
-rw-r--r--modules/chanserv/cs_fantasy_stats.cpp180
-rw-r--r--modules/chanserv/cs_fantasy_top.cpp194
-rw-r--r--modules/chanserv/cs_flags.cpp518
-rw-r--r--modules/chanserv/cs_getkey.cpp75
-rw-r--r--modules/chanserv/cs_info.cpp100
-rw-r--r--modules/chanserv/cs_invite.cpp113
-rw-r--r--modules/chanserv/cs_kick.cpp149
-rw-r--r--modules/chanserv/cs_list.cpp267
-rw-r--r--modules/chanserv/cs_log.cpp404
-rw-r--r--modules/chanserv/cs_mode.cpp1066
-rw-r--r--modules/chanserv/cs_register.cpp126
-rw-r--r--modules/chanserv/cs_seen.cpp461
-rw-r--r--modules/chanserv/cs_set.cpp1326
-rw-r--r--modules/chanserv/cs_set_misc.cpp222
-rw-r--r--modules/chanserv/cs_status.cpp125
-rw-r--r--modules/chanserv/cs_statusupdate.cpp68
-rw-r--r--modules/chanserv/cs_suspend.cpp296
-rw-r--r--modules/chanserv/cs_sync.cpp68
-rw-r--r--modules/chanserv/cs_topic.cpp273
-rw-r--r--modules/chanserv/cs_unban.cpp132
-rw-r--r--modules/chanserv/cs_updown.cpp245
-rw-r--r--modules/chanserv/cs_xop.cpp651
30 files changed, 10250 insertions, 0 deletions
diff --git a/modules/chanserv/chanserv.cpp b/modules/chanserv/chanserv.cpp
new file mode 100644
index 000000000..c47f2d77f
--- /dev/null
+++ b/modules/chanserv/chanserv.cpp
@@ -0,0 +1,481 @@
+/* ChanServ 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/cs_mode.h"
+
+inline static Anope::string BotModes()
+{
+ return Config->GetModule("botserv")->Get<Anope::string>("botmodes",
+ Config->GetModule("chanserv")->Get<Anope::string>("botmodes", "o")
+ );
+}
+
+class ChanServCore final
+ : public Module
+ , public ChanServService
+{
+ Reference<BotInfo> ChanServ;
+ std::vector<Anope::string> defaults;
+ ExtensibleItem<bool> inhabit;
+ ExtensibleRef<bool> persist;
+ bool always_lower = false;
+
+public:
+ ChanServCore(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, PSEUDOCLIENT | VENDOR),
+ ChanServService(this), inhabit(this, "inhabit"), persist("PERSIST")
+ {
+ }
+
+ void Hold(Channel *c) override
+ {
+ /** A timer used to keep the BotServ bot/ChanServ in the channel
+ * after kicking the last user in a channel
+ */
+ class ChanServTimer final
+ : public Timer
+ {
+ Reference<BotInfo> &ChanServ;
+ ExtensibleItem<bool> &inhabit;
+ Reference<Channel> c;
+
+ public:
+ /** Constructor
+ * @param chan The channel
+ */
+ ChanServTimer(Reference<BotInfo> &cs, ExtensibleItem<bool> &i, Module *m, Channel *chan) : Timer(m, Config->GetModule(m)->Get<time_t>("inhabit", "15s")), ChanServ(cs), inhabit(i), c(chan)
+ {
+ if (!ChanServ || !c)
+ return;
+ inhabit.Set(c, true);
+ if (!c->ci || !c->ci->bi)
+ ChanServ->Join(c);
+ else if (!c->FindUser(c->ci->bi))
+ c->ci->bi->Join(c);
+
+ /* Set +ntsi to prevent rejoin */
+ c->SetMode(NULL, "NOEXTERNAL");
+ c->SetMode(NULL, "TOPIC");
+ c->SetMode(NULL, "SECRET");
+ c->SetMode(NULL, "INVITE");
+ }
+
+ /** Called when the delay is up
+ * @param The current time
+ */
+ void Tick(time_t) override
+ {
+ if (!c)
+ return;
+
+ /* In the event we don't part */
+ c->RemoveMode(NULL, "SECRET");
+ c->RemoveMode(NULL, "INVITE");
+
+ inhabit.Unset(c); /* now we're done changing modes, unset inhabit */
+
+ if (!c->ci || !c->ci->bi)
+ {
+ if (ChanServ)
+ ChanServ->Part(c);
+ }
+ /* If someone has rejoined this channel in the meantime, don't part the bot */
+ else if (c->users.size() <= 1)
+ c->ci->bi->Part(c);
+ }
+ };
+
+ if (inhabit.HasExt(c))
+ return;
+
+ new ChanServTimer(ChanServ, inhabit, this->owner, c);
+ }
+
+ void OnReload(Configuration::Conf *conf) override
+ {
+ const Anope::string &channick = conf->GetModule(this)->Get<const Anope::string>("client");
+
+ if (channick.empty())
+ throw ConfigException(Module::name + ": <client> must be defined");
+
+ BotInfo *bi = BotInfo::Find(channick, true);
+ if (!bi)
+ throw ConfigException(Module::name + ": no bot named " + channick);
+
+ ChanServ = bi;
+
+ spacesepstream(conf->GetModule(this)->Get<const Anope::string>("defaults", "keeptopic peace cs_secure securefounder signkick")).GetTokens(defaults);
+ if (defaults.empty())
+ {
+ defaults.emplace_back("KEEPTOPIC");
+ defaults.emplace_back("PEACE");
+ defaults.emplace_back("CS_SECURE");
+ defaults.emplace_back("SECUREFOUNDER");
+ defaults.emplace_back("SIGNKICK");
+ }
+ else if (defaults[0].equals_ci("none"))
+ defaults.clear();
+
+ always_lower = conf->GetModule(this)->Get<bool>("always_lower_ts");
+ }
+
+ void OnBotDelete(BotInfo *bi) override
+ {
+ if (bi == ChanServ)
+ ChanServ = NULL;
+ }
+
+ EventReturn OnBotPrivmsg(User *u, BotInfo *bi, Anope::string &message) override
+ {
+ if (bi == ChanServ && Config->GetModule(this)->Get<bool>("opersonly") && !u->HasMode("OPER"))
+ {
+ u->SendMessage(bi, ACCESS_DENIED);
+ return EVENT_STOP;
+ }
+
+ return EVENT_CONTINUE;
+ }
+
+ void OnDelCore(NickCore *nc) override
+ {
+ std::deque<ChannelInfo *> chans;
+ nc->GetChannelReferences(chans);
+ int max_reg = Config->GetModule(this)->Get<int>("maxregistered");
+
+ for (auto *ci : chans)
+ {
+ if (ci->GetFounder() == nc)
+ {
+ NickCore *newowner = NULL;
+ if (ci->GetSuccessor() && ci->GetSuccessor() != nc && (ci->GetSuccessor()->IsServicesOper() || !max_reg || ci->GetSuccessor()->channelcount < max_reg))
+ newowner = ci->GetSuccessor();
+ else
+ {
+ const ChanAccess *highest = NULL;
+ for (unsigned j = 0; j < ci->GetAccessCount(); ++j)
+ {
+ const ChanAccess *ca = ci->GetAccess(j);
+ NickCore *anc = ca->GetAccount();
+
+ if (!anc || (!anc->IsServicesOper() && max_reg && anc->channelcount >= max_reg) || (anc == nc))
+ continue;
+ if (!highest || *ca > *highest)
+ highest = ca;
+ }
+ if (highest)
+ newowner = highest->GetAccount();
+ }
+
+ if (newowner)
+ {
+ Log(LOG_NORMAL, "chanserv/drop", ChanServ) << "Transferring foundership of " << ci->name << " from deleted nick " << nc->display << " to " << newowner->display;
+ ci->SetFounder(newowner);
+ ci->SetSuccessor(NULL);
+ }
+ else
+ {
+ Log(LOG_NORMAL, "chanserv/drop", ChanServ) << "Deleting channel " << ci->name << " owned by deleted nick " << nc->display;
+
+ delete ci;
+ continue;
+ }
+ }
+
+ if (ci->GetSuccessor() == nc)
+ ci->SetSuccessor(NULL);
+
+ for (unsigned j = 0; j < ci->GetAccessCount(); ++j)
+ {
+ const ChanAccess *ca = ci->GetAccess(j);
+
+ if (ca->GetAccount() == nc)
+ {
+ delete ci->EraseAccess(j);
+ break;
+ }
+ }
+
+ for (unsigned j = 0; j < ci->GetAkickCount(); ++j)
+ {
+ const AutoKick *akick = ci->GetAkick(j);
+ if (akick->nc == nc)
+ {
+ ci->EraseAkick(j);
+ break;
+ }
+ }
+ }
+ }
+
+ void OnDelChan(ChannelInfo *ci) override
+ {
+ /* remove access entries that are this channel */
+
+ std::deque<Anope::string> chans;
+ ci->GetChannelReferences(chans);
+
+ for (const auto &chan : chans)
+ {
+ ChannelInfo *c = ChannelInfo::Find(chan);
+ if (!c)
+ continue;
+
+ for (unsigned j = 0; j < c->GetAccessCount(); ++j)
+ {
+ ChanAccess *a = c->GetAccess(j);
+
+ if (a->Mask().equals_ci(ci->name))
+ {
+ delete a;
+ break;
+ }
+ }
+ }
+
+ if (ci->c)
+ {
+ ci->c->RemoveMode(ci->WhoSends(), "REGISTERED", "", false);
+
+ const Anope::string &require = Config->GetModule(this)->Get<const Anope::string>("require");
+ if (!require.empty())
+ ci->c->SetModes(ci->WhoSends(), false, "-%s", require.c_str());
+ }
+ }
+
+ EventReturn OnPreHelp(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+ if (!params.empty() || source.c || source.service != *ChanServ)
+ return EVENT_CONTINUE;
+ source.Reply(_("\002%s\002 allows you to register and control various\n"
+ "aspects of channels. %s can often prevent\n"
+ "malicious users from \"taking over\" channels by limiting\n"
+ "who is allowed channel operator privileges. Available\n"
+ "commands are listed below; to use them, type\n"
+ "\002%s%s \037command\037\002. For more information on a\n"
+ "specific command, type \002%s%s HELP \037command\037\002.\n"),
+ ChanServ->nick.c_str(), ChanServ->nick.c_str(), Config->StrictPrivmsg.c_str(), ChanServ->nick.c_str(), Config->StrictPrivmsg.c_str(), ChanServ->nick.c_str());
+ return EVENT_CONTINUE;
+ }
+
+ void OnPostHelp(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+ if (!params.empty() || source.c || source.service != *ChanServ)
+ return;
+ time_t chanserv_expire = Config->GetModule(this)->Get<time_t>("expire", "30d");
+ if (chanserv_expire >= 86400)
+ source.Reply(_(" \n"
+ "Note that any channel which is not used for %lu days\n"
+ "(i.e. which no user on the channel's access list enters\n"
+ "for that period of time) will be automatically dropped."), (unsigned long)chanserv_expire / 86400);
+ if (source.IsServicesOper())
+ source.Reply(_(" \n"
+ "Services Operators can also, depending on their access drop\n"
+ "any channel, view (and modify) the access, levels and akick\n"
+ "lists and settings for any channel."));
+ }
+
+ void OnCheckModes(Reference<Channel> &c) override
+ {
+ if (!c)
+ return;
+
+ if (c->ci)
+ c->SetMode(c->ci->WhoSends(), "REGISTERED", "", false);
+ else
+ c->RemoveMode(c->WhoSends(), "REGISTERED", "", false);
+
+ const Anope::string &require = Config->GetModule(this)->Get<const Anope::string>("require");
+ if (!require.empty())
+ {
+ if (c->ci)
+ c->SetModes(c->ci->WhoSends(), false, "+%s", require.c_str());
+ else
+ c->SetModes(c->WhoSends(), false, "-%s", require.c_str());
+ }
+ }
+
+ void OnCreateChan(ChannelInfo *ci) override
+ {
+ /* Set default chan flags */
+ for (const auto &def : defaults)
+ ci->Extend<bool>(def.upper());
+ }
+
+ EventReturn OnCanSet(User *u, const ChannelMode *cm) override
+ {
+ if (Config->GetModule(this)->Get<const Anope::string>("nomlock").find(cm->mchar) != Anope::string::npos
+ || Config->GetModule(this)->Get<const Anope::string>("require").find(cm->mchar) != Anope::string::npos)
+ return EVENT_STOP;
+ return EVENT_CONTINUE;
+ }
+
+ void OnChannelSync(Channel *c) override
+ {
+ bool perm = c->HasMode("PERM") || (c->ci && persist && persist->HasExt(c->ci));
+ if (!perm && !c->botchannel && (c->users.empty() || (c->users.size() == 1 && c->users.begin()->second->user->server == Me)))
+ {
+ this->Hold(c);
+ }
+ }
+
+ void OnLog(Log *l) override
+ {
+ if (l->type == LOG_CHANNEL)
+ l->bi = ChanServ;
+ }
+
+ void OnExpireTick() override
+ {
+ time_t chanserv_expire = Config->GetModule(this)->Get<time_t>("expire", "30d");
+
+ if (!chanserv_expire || Anope::NoExpire || Anope::ReadOnly)
+ return;
+
+ for (registered_channel_map::const_iterator it = RegisteredChannelList->begin(), it_end = RegisteredChannelList->end(); it != it_end; )
+ {
+ ChannelInfo *ci = it->second;
+ ++it;
+
+ bool expire = false;
+
+ if (Anope::CurTime - ci->last_used >= chanserv_expire)
+ {
+ if (ci->c)
+ {
+ time_t last_used = ci->last_used;
+ for (Channel::ChanUserList::const_iterator cit = ci->c->users.begin(), cit_end = ci->c->users.end(); cit != cit_end && last_used == ci->last_used; ++cit)
+ ci->AccessFor(cit->second->user);
+ expire = last_used == ci->last_used;
+ }
+ else
+ expire = true;
+ }
+
+ FOREACH_MOD(OnPreChanExpire, (ci, expire));
+
+ if (expire)
+ {
+ Log(LOG_NORMAL, "chanserv/expire", ChanServ) << "Expiring channel " << ci->name << " (founder: " << (ci->GetFounder() ? ci->GetFounder()->display : "(none)") << ")";
+ FOREACH_MOD(OnChanExpire, (ci));
+ delete ci;
+ }
+ }
+ }
+
+ EventReturn OnCheckDelete(Channel *c) override
+ {
+ /* Do not delete this channel if ChanServ/a BotServ bot is inhabiting it */
+ if (inhabit.HasExt(c))
+ return EVENT_STOP;
+
+ return EVENT_CONTINUE;
+ }
+
+ void OnPostInit() override
+ {
+ if (!persist)
+ return;
+
+ ChannelMode *perm = ModeManager::FindChannelModeByName("PERM");
+
+ /* Find all persistent channels and create them, as we are about to finish burst to our uplink */
+ for (const auto &[_, ci] : *RegisteredChannelList)
+ {
+ if (!persist->HasExt(ci))
+ continue;
+
+ bool c;
+ ci->c = Channel::FindOrCreate(ci->name, c, ci->time_registered);
+ ci->c->syncing |= created;
+
+ if (perm)
+ {
+ ci->c->SetMode(NULL, perm);
+ }
+ else
+ {
+ if (!ci->bi)
+ ci->WhoSends()->Assign(NULL, ci);
+ if (ci->c->FindUser(ci->bi) == NULL)
+ {
+ ChannelStatus status(BotModes());
+ ci->bi->Join(ci->c, &status);
+ }
+ }
+ }
+
+ }
+
+ void OnChanRegistered(ChannelInfo *ci) override
+ {
+ if (!persist || !ci->c)
+ return;
+ /* Mark the channel as persistent */
+ if (ci->c->HasMode("PERM"))
+ persist->Set(ci);
+ /* Persist may be in def cflags, set it here */
+ else if (persist->HasExt(ci))
+ ci->c->SetMode(NULL, "PERM");
+ }
+
+ void OnJoinChannel(User *u, Channel *c) override
+ {
+ if (always_lower && c->ci && c->creation_time > c->ci->time_registered)
+ {
+ Log(LOG_DEBUG) << "Changing TS of " << c->name << " from " << c->creation_time << " to " << c->ci->time_registered;
+ c->creation_time = c->ci->time_registered;
+ IRCD->SendChannel(c);
+ c->Reset();
+ }
+ }
+
+ EventReturn OnChannelModeSet(Channel *c, MessageSource &setter, ChannelMode *mode, const Anope::string &param) override
+ {
+ if (!always_lower && Anope::CurTime == c->creation_time && c->ci && setter.GetUser() && !setter.GetUser()->server->IsULined())
+ {
+ ChanUserContainer *cu = c->FindUser(setter.GetUser());
+ ChannelMode *cm = ModeManager::FindChannelModeByName("OP");
+ if (cu && cm && !cu->status.HasMode(cm->mchar))
+ {
+ /* Our -o and their mode change crossing, bounce their mode */
+ c->RemoveMode(c->ci->WhoSends(), mode, param);
+ /* We don't set mlocks until after the join has finished processing, it will stack with this change,
+ * so there isn't much for the user to remove except -nt etc which is likely locked anyway.
+ */
+ }
+ }
+
+ return EVENT_CONTINUE;
+ }
+
+ void OnChanInfo(CommandSource &source, ChannelInfo *ci, InfoFormatter &info, bool show_all) override
+ {
+ if (!show_all)
+ return;
+
+ time_t chanserv_expire = Config->GetModule(this)->Get<time_t>("expire", "30d");
+ if (!ci->HasExt("CS_NO_EXPIRE") && chanserv_expire && !Anope::NoExpire && ci->last_used != Anope::CurTime)
+ info[_("Expires")] = Anope::strftime(ci->last_used + chanserv_expire, source.GetAccount());
+ }
+
+ void OnSetCorrectModes(User *user, Channel *chan, AccessGroup &access, bool &give_modes, bool &take_modes) override
+ {
+ if (always_lower)
+ // Since we always lower the TS, the other side will remove the modes if the channel ts lowers, so we don't
+ // have to worry about it
+ take_modes = false;
+ else if (ModeManager::FindChannelModeByName("REGISTERED"))
+ // Otherwise if the registered channel mode exists, we should remove modes if the channel is not +r
+ take_modes = !chan->HasMode("REGISTERED");
+ }
+};
+
+MODULE_INIT(ChanServCore)
diff --git a/modules/chanserv/cs_access.cpp b/modules/chanserv/cs_access.cpp
new file mode 100644
index 000000000..7625f05aa
--- /dev/null
+++ b/modules/chanserv/cs_access.cpp
@@ -0,0 +1,939 @@
+/* ChanServ 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"
+
+static std::map<Anope::string, int16_t, ci::less> defaultLevels;
+
+static inline void reset_levels(ChannelInfo *ci)
+{
+ ci->ClearLevels();
+ for (auto &[priv, level] : defaultLevels)
+ ci->SetLevel(priv, level);
+}
+
+class AccessChanAccess final
+ : public ChanAccess
+{
+public:
+ int level = 0;
+
+ AccessChanAccess(AccessProvider *p) : ChanAccess(p)
+ {
+ }
+
+ bool HasPriv(const Anope::string &name) const override
+ {
+ return this->ci->GetLevel(name) != ACCESS_INVALID && this->level >= this->ci->GetLevel(name);
+ }
+
+ Anope::string AccessSerialize() const override
+ {
+ return stringify(this->level);
+ }
+
+ void AccessUnserialize(const Anope::string &data) override
+ {
+ try
+ {
+ this->level = convertTo<int>(data);
+ }
+ catch (const ConvertException &)
+ {
+ }
+ }
+
+ bool operator>(const ChanAccess &other) const override
+ {
+ if (this->provider != other.provider)
+ return ChanAccess::operator>(other);
+ else
+ return this->level > anope_dynamic_static_cast<const AccessChanAccess *>(&other)->level;
+ }
+
+ bool operator<(const ChanAccess &other) const override
+ {
+ if (this->provider != other.provider)
+ return ChanAccess::operator<(other);
+ else
+ return this->level < anope_dynamic_static_cast<const AccessChanAccess *>(&other)->level;
+ }
+};
+
+class AccessAccessProvider final
+ : public AccessProvider
+{
+public:
+ static AccessAccessProvider *me;
+
+ AccessAccessProvider(Module *o) : AccessProvider(o, "access/access")
+ {
+ me = this;
+ }
+
+ ChanAccess *Create() override
+ {
+ return new AccessChanAccess(this);
+ }
+};
+AccessAccessProvider* AccessAccessProvider::me;
+
+class CommandCSAccess final
+ : public Command
+{
+ void DoAdd(CommandSource &source, ChannelInfo *ci, const std::vector<Anope::string> &params)
+ {
+ Anope::string mask = params[2];
+ Privilege *p = NULL;
+ int level = ACCESS_INVALID;
+
+ try
+ {
+ level = convertTo<int>(params[3]);
+ }
+ catch (const ConvertException &)
+ {
+ p = PrivilegeManager::FindPrivilege(params[3]);
+ if (p != NULL && defaultLevels[p->name])
+ level = defaultLevels[p->name];
+ }
+
+ if (!level)
+ {
+ source.Reply(_("Access level must be non-zero."));
+ return;
+ }
+ else if (level <= ACCESS_INVALID || level >= ACCESS_FOUNDER)
+ {
+ source.Reply(CHAN_ACCESS_LEVEL_RANGE, ACCESS_INVALID + 1, ACCESS_FOUNDER - 1);
+ return;
+ }
+
+ AccessGroup u_access = source.AccessFor(ci);
+ const ChanAccess *highest = u_access.Highest();
+
+ AccessChanAccess tmp_access(AccessAccessProvider::me);
+ tmp_access.ci = ci;
+ tmp_access.level = level;
+
+ bool override = false;
+ const NickAlias *na = NULL;
+
+ if ((!highest || *highest <= tmp_access) && !u_access.founder)
+ {
+ if (source.HasPriv("chanserv/access/modify"))
+ override = true;
+ else
+ {
+ source.Reply(ACCESS_DENIED);
+ return;
+ }
+ }
+
+ if (IRCD->IsChannelValid(mask))
+ {
+ if (Config->GetModule("chanserv")->Get<bool>("disallow_channel_access"))
+ {
+ source.Reply(_("Channels may not be on access lists."));
+ return;
+ }
+
+ ChannelInfo *targ_ci = ChannelInfo::Find(mask);
+ if (targ_ci == NULL)
+ {
+ source.Reply(CHAN_X_NOT_REGISTERED, mask.c_str());
+ return;
+ }
+ else if (ci == targ_ci)
+ {
+ source.Reply(_("You can't add a channel to its own access list."));
+ return;
+ }
+
+ mask = targ_ci->name;
+ }
+ else
+ {
+ na = NickAlias::Find(mask);
+
+ if (!na && Config->GetModule("chanserv")->Get<bool>("disallow_hostmask_access"))
+ {
+ source.Reply(_("Masks and unregistered users may not be on access lists."));
+ return;
+ }
+ else if (na && na->nc->HasExt("NEVEROP"))
+ {
+ source.Reply(_("\002%s\002 does not wish to be added to channel access lists."),
+ na->nc->display.c_str());
+ return;
+ }
+ else if (mask.find_first_of("!*@") == Anope::string::npos && !na)
+ {
+ User *targ = User::Find(mask, true);
+ if (targ != NULL)
+ mask = "*!*@" + targ->GetDisplayedHost();
+ else
+ {
+ source.Reply(NICK_X_NOT_REGISTERED, mask.c_str());
+ return;
+ }
+ }
+
+ if (na)
+ mask = na->nick;
+ }
+
+ for (unsigned i = ci->GetAccessCount(); i > 0; --i)
+ {
+ const ChanAccess *access = ci->GetAccess(i - 1);
+ if ((na && na->nc == access->GetAccount()) || mask.equals_ci(access->Mask()))
+ {
+ /* Don't allow lowering from a level >= u_level */
+ if ((!highest || *access >= *highest) && !u_access.founder && !source.HasPriv("chanserv/access/modify"))
+ {
+ source.Reply(ACCESS_DENIED);
+ return;
+ }
+ delete ci->EraseAccess(i - 1);
+ break;
+ }
+ }
+
+ unsigned access_max = Config->GetModule("chanserv")->Get<unsigned>("accessmax", "1024");
+ if (access_max && ci->GetDeepAccessCount() >= access_max)
+ {
+ source.Reply(_("Sorry, you can only have %d access entries on a channel, including access entries from other channels."), access_max);
+ return;
+ }
+
+ ServiceReference<AccessProvider> provider("AccessProvider", "access/access");
+ if (!provider)
+ return;
+ AccessChanAccess *access = anope_dynamic_static_cast<AccessChanAccess *>(provider->Create());
+ access->SetMask(mask, ci);
+ access->creator = source.GetNick();
+ access->level = level;
+ access->last_seen = 0;
+ access->created = Anope::CurTime;
+ access->description = params.size() > 4 ? params[4] : "";
+ ci->AddAccess(access);
+
+ FOREACH_MOD(OnAccessAdd, (ci, source, access));
+
+ Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to add " << mask << " with level " << level;
+ if (p != NULL)
+ source.Reply(_("\002%s\002 added to %s access list at privilege %s (level %d)"), access->Mask().c_str(), ci->name.c_str(), p->name.c_str(), level);
+ else
+ source.Reply(_("\002%s\002 added to %s access list at level \002%d\002."), access->Mask().c_str(), ci->name.c_str(), level);
+ }
+
+ void DoDel(CommandSource &source, ChannelInfo *ci, const std::vector<Anope::string> &params)
+ {
+ Anope::string mask = params[2];
+
+ const NickAlias *na = NickAlias::Find(mask);
+ if (na && na->nc)
+ {
+ mask = na->nc->display;
+ }
+ else if (!isdigit(mask[0]) && mask.find_first_of("#!*@") == Anope::string::npos)
+ {
+ User *targ = User::Find(mask, true);
+ if (targ != NULL)
+ mask = "*!*@" + targ->GetDisplayedHost();
+ else
+ {
+ source.Reply(NICK_X_NOT_REGISTERED, mask.c_str());
+ return;
+ }
+ }
+
+ if (!ci->GetAccessCount())
+ source.Reply(_("%s access list is empty."), ci->name.c_str());
+ else if (isdigit(mask[0]) && mask.find_first_not_of("1234567890,-") == Anope::string::npos)
+ {
+ class AccessDelCallback final
+ : public NumberList
+ {
+ CommandSource &source;
+ ChannelInfo *ci;
+ Command *c;
+ unsigned deleted = 0;
+ Anope::string Nicks;
+ bool denied = false;
+ bool override = false;
+ public:
+ AccessDelCallback(CommandSource &_source, ChannelInfo *_ci, Command *_c, const Anope::string &numlist) : NumberList(numlist, true), source(_source), ci(_ci), c(_c)
+ {
+ if (!source.AccessFor(ci).HasPriv("ACCESS_CHANGE") && source.HasPriv("chanserv/access/modify"))
+ this->override = true;
+ }
+
+ ~AccessDelCallback() override
+ {
+ if (denied && !deleted)
+ source.Reply(ACCESS_DENIED);
+ else if (!deleted)
+ source.Reply(_("No matching entries on %s access list."), ci->name.c_str());
+ else
+ {
+ Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, c, ci) << "to delete " << Nicks;
+
+ if (deleted == 1)
+ source.Reply(_("Deleted 1 entry from %s access list."), ci->name.c_str());
+ else
+ source.Reply(_("Deleted %d entries from %s access list."), deleted, ci->name.c_str());
+ }
+ }
+
+ void HandleNumber(unsigned Number) override
+ {
+ if (!Number || Number > ci->GetAccessCount())
+ return;
+
+ ChanAccess *access = ci->GetAccess(Number - 1);
+
+ AccessGroup ag = source.AccessFor(ci);
+ const ChanAccess *u_highest = ag.Highest();
+
+ if ((!u_highest || *u_highest <= *access) && !ag.founder && !this->override && access->GetAccount() != source.nc)
+ {
+ denied = true;
+ return;
+ }
+
+ ++deleted;
+ if (!Nicks.empty())
+ Nicks += ", " + access->Mask();
+ else
+ Nicks = access->Mask();
+
+ ci->EraseAccess(Number - 1);
+
+ FOREACH_MOD(OnAccessDel, (ci, source, access));
+ delete access;
+ }
+ }
+ delcallback(source, ci, this, mask);
+ delcallback.Process();
+ }
+ else
+ {
+ AccessGroup u_access = source.AccessFor(ci);
+ const ChanAccess *highest = u_access.Highest();
+
+ for (unsigned i = ci->GetAccessCount(); i > 0; --i)
+ {
+ ChanAccess *access = ci->GetAccess(i - 1);
+ if (mask.equals_ci(access->Mask()))
+ {
+ if (access->GetAccount() != source.nc && !u_access.founder && (!highest || *highest <= *access) && !source.HasPriv("chanserv/access/modify"))
+ source.Reply(ACCESS_DENIED);
+ else
+ {
+ source.Reply(_("\002%s\002 deleted from %s access list."), access->Mask().c_str(), ci->name.c_str());
+ bool override = !u_access.founder && !u_access.HasPriv("ACCESS_CHANGE") && access->GetAccount() != source.nc;
+ Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to delete " << access->Mask();
+
+ ci->EraseAccess(i - 1);
+ FOREACH_MOD(OnAccessDel, (ci, source, access));
+ delete access;
+ }
+ return;
+ }
+ }
+
+ source.Reply(_("\002%s\002 not found on %s access list."), mask.c_str(), ci->name.c_str());
+ }
+
+ return;
+ }
+
+ void ProcessList(CommandSource &source, ChannelInfo *ci, const std::vector<Anope::string> &params, ListFormatter &list)
+ {
+ const Anope::string &nick = params.size() > 2 ? params[2] : "";
+
+ if (!ci->GetAccessCount())
+ source.Reply(_("%s access list is empty."), ci->name.c_str());
+ else if (!nick.empty() && nick.find_first_not_of("1234567890,-") == Anope::string::npos)
+ {
+ class AccessListCallback final
+ : public NumberList
+ {
+ ListFormatter &list;
+ ChannelInfo *ci;
+
+ public:
+ AccessListCallback(ListFormatter &_list, ChannelInfo *_ci, const Anope::string &numlist) : NumberList(numlist, false), list(_list), ci(_ci)
+ {
+ }
+
+ void H