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/chanserv | |
| parent | 7ac1fe58478d58e2480b6919c4abf3a82929169c (diff) | |
Shuffle modules around a bit.
Diffstat (limited to 'modules/chanserv')
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> ¶ms) 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> ¶ms) 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 ¶m) 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> ¶ms) + { + 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> ¶ms) + { + 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> ¶ms, 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 |
