/* * Anope IRC Services * * Copyright (C) 2003-2017 Anope Team * * This file is part of Anope. Anope is free software; you can * redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software * Foundation, version 2. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see see . */ /* Dependencies: anope_chanserv.main */ #include "module.h" #include "modules/chanserv.h" #include "modules/chanserv/access.h" #include "modules/chanserv/main/chanaccess.h" #include "main/chanaccesstype.h" class AccessChanAccessImpl : public AccessChanAccess { friend class AccessChanAccessType; Serialize::Storage level; public: static constexpr const char *NAME = "accesschanaccess"; using AccessChanAccess::AccessChanAccess; int GetLevel() override; void SetLevel(const int &) override; bool HasPriv(const Anope::string &name) override { return this->GetChannel()->GetLevel(name) != ChanServ::ACCESS_INVALID && this->GetLevel() >= this->GetChannel()->GetLevel(name); } Anope::string AccessSerialize() override { return stringify(this->GetLevel()); } void AccessUnserialize(const Anope::string &data) override { try { this->SetLevel(convertTo(data)); } catch (const ConvertException &) { } } int Compare(ChanAccess *other) override { if (this->GetSerializableType() != other->GetSerializableType()) return ChanAccessImpl::Compare(other); int lev = this->GetLevel(); int theirlev = anope_dynamic_static_cast(other)->GetLevel(); if (lev > theirlev) return 1; else if (lev < theirlev) return -1; else return 0; } }; class AccessChanAccessType : public ChanAccessType { public: Serialize::Field level; AccessChanAccessType(Module *me) : ChanAccessType(me) , level(this, "level", &AccessChanAccessImpl::level) { Serialize::SetParent(AccessChanAccess::NAME, ChanServ::ChanAccess::NAME); } }; int AccessChanAccessImpl::GetLevel() { return Get(&AccessChanAccessType::level); } void AccessChanAccessImpl::SetLevel(const int &i) { Object::Set(&AccessChanAccessType::level, i); } class CommandCSAccess : public Command { void DoAdd(CommandSource &source, ChanServ::Channel *ci, Anope::string mask, const Anope::string &levelstr) { ChanServ::Privilege *p = NULL; int level = ChanServ::ACCESS_INVALID; if (levelstr.empty()) { this->OnSyntaxError(source, "ADD"); return; } try { level = convertTo(levelstr); } catch (const ConvertException &) { p = ChanServ::service ? ChanServ::service->FindPrivilege(levelstr) : nullptr; if (p != NULL && p->level) level = p->level; } if (!level) { source.Reply(_("Access level must be non-zero.")); return; } if (level <= ChanServ::ACCESS_INVALID || level >= ChanServ::ACCESS_FOUNDER) { source.Reply(_("Access level must be between \002{0}\002 and \002{1}\002 inclusive."), ChanServ::ACCESS_INVALID + 1, ChanServ::ACCESS_FOUNDER - 1); return; } ChanServ::AccessGroup u_access = source.AccessFor(ci); ChanServ::ChanAccess *highest = u_access.Highest(); AccessChanAccess *access = Serialize::New(); access->SetChannel(ci); access->SetLevel(level); if ((!highest || *highest <= *access) && !u_access.founder) { if (!source.HasOverridePriv("chanserv/access/modify")) { source.Reply(_("Access denied. You do not have enough privileges on \002{0}\002 to add someone at level \002{1}\002."), ci->GetName(), level); access->Delete(); return; } } access->Delete(); NickServ::Nick *na = NickServ::FindNick(mask); if (!na && Config->GetModule("chanserv/main")->Get("disallow_hostmask_access")) { source.Reply(_("Masks and unregistered users may not be on access lists.")); return; } if (mask.find_first_of("!*@") == Anope::string::npos && !na) { User *targ = User::Find(mask, true); if (targ != NULL) mask = "*!*@" + targ->GetDisplayedHost(); else { source.Reply(_("\002{0}\002 isn't registered."), mask); return; } } if (na) mask = na->GetNick(); for (unsigned i = ci->GetAccessCount(); i > 0; --i) { ChanServ::ChanAccess *a = ci->GetAccess(i - 1); if (mask.equals_ci(a->Mask())) { /* Don't allow lowering from a level >= u_level */ if ((!highest || *a >= *highest) && !u_access.founder && !source.HasOverridePriv("chanserv/access/modify")) { source.Reply(_("Access denied. You do not have enough privileges on \002{0}\002 to lower the access of \002{1}\002."), ci->GetName(), a->Mask()); return; } a->Delete(); break; } } unsigned access_max = Config->GetModule("chanserv/main")->Get("accessmax", "1024"); if (access_max && ci->GetAccessCount() >= access_max) { source.Reply(_("Sorry, you can only have {0} access entries on a channel, including access entries from other channels."), access_max); return; } access = Serialize::New(); if (na) access->SetAccount(na->GetAccount()); access->SetChannel(ci); access->SetMask(mask); access->SetCreator(source.GetNick()); access->SetLevel(level); access->SetLastSeen(0); access->SetCreated(Anope::CurTime); EventManager::Get()->Dispatch(&Event::AccessAdd::OnAccessAdd, ci, source, access); logger.Command(source, ci, _("{source} used {command} on {channel} to add {0} with level {1}"), mask, level); if (p != NULL) source.Reply(_("\002{0}\002 added to the access list of \002{1}\002 with privilege \002{2}\002 (level \002{3}\002)."), access->Mask(), ci->GetName(), p->name, level); else source.Reply(_("\002{0}\002 added to the access list of \002{1}\002 at level \002{2}\002."), access->Mask(), ci->GetName(), level); } void DoDel(CommandSource &source, ChanServ::Channel *ci, Anope::string mask) { if (mask.empty()) { this->OnSyntaxError(source, "DEL"); return; } if (!ci->GetAccessCount()) { source.Reply(_("The access list for \002{0}\002 is empty."), ci->GetName()); return; } if (!isdigit(mask[0]) && mask.find_first_of("#!*@") == Anope::string::npos && !NickServ::FindNick(mask)) { User *targ = User::Find(mask, true); if (targ != NULL) mask = "*!*@" + targ->GetDisplayedHost(); else { source.Reply(_("\002{0}\002 isn't registered."), mask); return; } } if (isdigit(mask[0]) && mask.find_first_not_of("1234567890,-") == Anope::string::npos) { Anope::string nicks; bool denied = false; unsigned int deleted = 0; NumberList(mask, true, [&](unsigned int num) { if (!num || num > ci->GetAccessCount()) return; ChanServ::ChanAccess *access = ci->GetAccess(num - 1); ChanServ::AccessGroup ag = source.AccessFor(ci); ChanServ::ChanAccess *u_highest = ag.Highest(); if ((!u_highest || *u_highest <= *access) && !ag.founder && !source.IsOverride() && access->GetAccount() != source.nc) { denied = true; return; } ++deleted; if (!nicks.empty()) nicks += ", " + access->Mask(); else nicks = access->Mask(); EventManager::Get()->Dispatch(&Event::AccessDel::OnAccessDel, ci, source, access); access->Delete(); }, [&]() { if (denied && !deleted) source.Reply(_("Access denied. You do not have enough privileges on \002{0}\002 to remove any access entries matching \002{1}\002.")); else if (!deleted) source.Reply(_("There are no entries matching \002{0}\002 on the access list of \002{1}\002."), mask, ci->GetName()); else { logger.Command(source, ci, _("{source} used {command} on {channel} to delete {0}"), mask); if (deleted == 1) source.Reply(_("Deleted \0021\002 entry from the access list of \002{0}\002."), ci->GetName()); else source.Reply(_("Deleted \002{0}\002 entries from the access list of \002{1}\002."), deleted, ci->GetName()); } }); } else { ChanServ::AccessGroup u_access = source.AccessFor(ci); ChanServ::ChanAccess *highest = u_access.Highest(); for (unsigned i = ci->GetAccessCount(); i > 0; --i) { ChanServ::ChanAccess *access = ci->GetAccess(i - 1); if (mask.equals_ci(access->Mask())) { if (access->GetAccount() != source.nc && !u_access.founder && (!highest || *highest <= *access) && !source.HasOverridePriv("chanserv/access/modify")) { source.Reply(_("Access denied. You do not have enough privileges on \002{0}\002 to remove the access of \002{1}\002."), ci->GetName(), access->Mask()); } else { source.Reply(_("\002{0}\002 deleted from the access list of \002{1}\002."), access->Mask(), ci->GetName()); logger.Command(source, ci, _("{source} used {command} on {channel} to delete {3}"), access->Mask()); EventManager::Get()->Dispatch(&Event::AccessDel::OnAccessDel, ci, source, access); access->Delete(); } return; } } source.Reply(_("\002{0}\002 was not found on the access list of \002{1}\002."), mask, ci->GetName()); } } void ProcessList(CommandSource &source, ChanServ::Channel *ci, const Anope::string &nick, ListFormatter &list) { if (!ci->GetAccessCount()) { source.Reply(_("The access list for \002{0}\002 is empty."), ci->GetName()); return; } if (!nick.empty() && nick.find_first_not_of("1234567890,-") == Anope::string::npos) { NumberList(nick, false, [&](unsigned int number) { if (!number || number > ci->GetAccessCount()) return; ChanServ::ChanAccess *access = ci->GetAccess(number - 1); Anope::string timebuf; if (Channel *c = ci->GetChannel()) for (Channel::ChanUserList::const_iterator cit = c->users.begin(), cit_end = c->users.end(); cit != cit_end; ++cit) { if (access->Matches(cit->second->user, cit->second->user->Account())) timebuf = "Now"; } if (timebuf.empty()) { if (access->GetLastSeen() == 0) timebuf = "Never"; else timebuf = Anope::strftime(access->GetLastSeen(), NULL, true); } ListFormatter::ListEntry entry; entry["Number"] = stringify(number); entry["Level"] = access->AccessSerialize(); entry["Mask"] = access->Mask(); entry["By"] = access->GetCreator(); entry["Last seen"] = timebuf; list.AddEntry(entry); }, [&](){}); } else { for (unsigned i = 0, end = ci->GetAccessCount(); i < end; ++i) { ChanServ::ChanAccess *access = ci->GetAccess(i); if (!nick.empty() && !Anope::Match(access->Mask(), nick)) continue; Anope::string timebuf; if (Channel *c = ci->GetChannel()) for (Channel::ChanUserList::const_iterator cit = c->users.begin(), cit_end = c->users.end(); cit != cit_end; ++cit) { if (access->Matches(cit->second->user, cit->second->user->Account())) timebuf = "Now"; } if (timebuf.empty()) { if (access->GetLastSeen() == 0) timebuf = "Never"; else timebuf = Anope::strftime(access->GetLastSeen(), NULL, true); } ListFormatter::ListEntry entry; entry["Number"] = stringify(i + 1); entry["Level"] = access->AccessSerialize(); entry["Mask"] = access->Mask(); entry["By"] = access->GetCreator(); entry["Last seen"] = timebuf; list.AddEntry(entry); } } if (list.IsEmpty()) { source.Reply(_("No matching entries on the access list of \002{0}\002."), ci->GetName()); return; } std::vector replies; list.Process(replies); source.Reply(_("Access list for \002{0}\002:"), ci->GetName()); for (unsigned i = 0; i < replies.size(); ++i) source.Reply(replies[i]); source.Reply(_("End of access list.")); } void DoList(CommandSource &source, ChanServ::Channel *ci, const Anope::string &nick) { if (!ci->GetAccessCount()) { source.Reply(_("The access list for \002{0}\002 is empty."), ci->GetName()); return; } ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Level")).AddColumn(_("Mask")); this->ProcessList(source, ci, nick, list); } void DoView(CommandSource &source, ChanServ::Channel *ci, const Anope::string &nick) { if (!ci->GetAccessCount()) { source.Reply(_("The access list for \002{0}\002 is empty."), ci->GetName()); return; } ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Level")).AddColumn(_("Mask")).AddColumn(_("By")).AddColumn(_("Last seen")); this->ProcessList(source, ci, nick, list); } void DoClear(CommandSource &source, ChanServ::Channel *ci) { if (!source.IsFounder(ci) && !source.HasOverridePriv("chanserv/access/modify")) { source.Reply(_("Access denied. You do not have privilege \002{0}\002 on \002{1}\002."), "FOUNDER", ci->GetName()); return; } EventManager::Get()->Dispatch(&Event::AccessClear::OnAccessClear, ci, source); ci->ClearAccess(); source.Reply(_("The access list of \002{0}\002 has been cleared."), ci->GetName()); logger.Command(source, ci, _("{source} used {command} on {channel} to clear the access list")); } public: CommandCSAccess(Module *creator) : Command(creator, "chanserv/access", 2, 4) { this->SetDesc(_("Modify the list of privileged users")); this->SetSyntax(_("\037channel\037 ADD \037mask\037 \037level\037")); this->SetSyntax(_("\037channel\037 DEL {\037mask\037 | \037entry-num\037 | \037list\037}")); this->SetSyntax(_("\037channel\037 LIST [\037mask\037 | \037list\037]")); this->SetSyntax(_("\037channel\037 VIEW [\037mask\037 | \037list\037]")); this->SetSyntax(_("\037channel\037 CLEAR")); } void Execute(CommandSource &source, const std::vector ¶ms) override { const Anope::string &chan = params[0]; const Anope::string &cmd = params[1]; const Anope::string &nick = params.size() > 2 ? params[2] : ""; const Anope::string &level = params.size() > 3 ? params[3] : ""; ChanServ::Channel *ci = ChanServ::Find(chan); if (ci == nullptr) { source.Reply(_("Channel \002{0}\002 isn't registered."), chan); return; } bool is_list = cmd.equals_ci("LIST") || cmd.equals_ci("VIEW"); bool is_del = cmd.equals_ci("DEL"); ChanServ::AccessGroup access = source.AccessFor(ci); bool has_access = false; if (access.HasPriv("ACCESS_CHANGE")) { has_access = true; } else if (is_list && access.HasPriv("ACCESS_LIST")) { has_access = true; } else if (is_del) { NickServ::Nick *na = NickServ::FindNick(nick); if (na && na->GetAccount() == source.GetAccount()) has_access = true; } if (!has_access) { if (source.HasOverridePriv("chanserv/access/modify")) has_access = true; else if (is_list && source.HasOverridePriv("chanserv/access/list")) has_access = true; } if (!has_access) { source.Reply(_("Access denied. You do not have privilege \002{0}\002 on \002{1}\002."), is_list ? "ACCESS_LIST" : "ACCESS_CHANGE", ci->GetName()); return; } if (Anope::ReadOnly && !is_list) { source.Reply(_("Sorry, channel access list modification is temporarily disabled.")); return; } if (cmd.equals_ci("ADD")) this->DoAdd(source, ci, nick, level); else if (cmd.equals_ci("DEL")) this->DoDel(source, ci, nick); else if (cmd.equals_ci("LIST")) this->DoList(source, ci, nick); else if (cmd.equals_ci("VIEW")) this->DoView(source, ci, nick); else if (cmd.equals_ci("CLEAR")) this->DoClear(source, ci); else this->OnSyntaxError(source, ""); } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { if (subcommand.equals_ci("ADD")) { source.Reply(_("The \002{0} ADD\002 adds \037mask\037 to the access list of \037channel\037 at level \037level\037." " If \037mask\037 is already present on the access list, the access level for it is changed to \037level\037." " The \037level\037 may be a numerical level between \002{1}\002 and \002{2}\002 or the name of a privilege (eg. \002{3}\002)." " The privilege set granted to a given user is the union of the privileges of access entries that match the user." " Use of this command requires the \002{4}\002 privilege on \037channel\037."), source.GetCommand(), ChanServ::ACCESS_INVALID + 1, ChanServ::ACCESS_FOUNDER - 1, "AUTOOP", "ACCESS_CHANGE"); //XXX show def levels source.Reply(_("\n" "Examples:\n" " {command} #anope ADD Adam 9001\n" " Adds \"Adam\" to the access list of \"#anope\" at level \"9001\".\n" "\n" " {command} #anope ADD *!*@anope.org AUTOOP\n" " Adds the host mask \"*!*@anope.org\" to the access list of \"#anope\" with the privilege \"AUTOOP\".")); } else if (subcommand.equals_ci("DEL")) source.Reply(_("The \002{0} DEL\002 command removes \037mask\037 from the access list of \037channel\037." " If a list of entry numbers is given, those entries are deleted." " You may remove yourself from an access list, even if you do not have access to modify that list otherwise." " Use of this command requires the \002{1}\002 privilege on \037channel\037.\n" "\n" "Example:\n" " {command} #anope del DukePyrolator\n" " Removes the access of \"DukePyrolator\" from \"#anope\"."), source.GetCommand(), "ACCESS_CHANGE"); else if (subcommand.equals_ci("LIST") || subcommand.equals_ci("VIEW")) source.Reply(_("The \002{0} LIST\002 and \002{0} VIEW\002 command displays the access list of \037channel\037." " If a wildcard mask is given, only those entries matching the mask are displayed." " If a list of entry numbers is given, only those entries are shown." " \002VIEW\002 is similar to \002LIST\002 but also shows who created the access entry, and when the access entry was last used." " Use of these commands requires the \002{1}\002 privilege on \037channel\037.\n" "\n" "Example:\n" " {0} #anope LIST 2-5,7-9\n" " Lists access entries numbered 2 through 5 and 7 through 9 on #anope."), source.GetCommand(), "ACCESS_LIST"); else if (subcommand.equals_ci("CLEAR")) source.Reply(_("The \002{0} CLEAR\002 command clears the access list of \037channel\037." " Use of this command requires the \002{1}\002 privilege on \037channel\037."), source.GetCommand(), "FOUNDER"); else { source.Reply(_("Maintains the access list for \037channel\037. The access list specifies which users are granted which privileges to the channel." " The access system uses numerical levels to represent different sets of privileges. Users who are identified but do not match any entries on" " the access list has a level of 0. Unregistered or unidentified users who do not match any entries have a user level of 0.")); ServiceBot *bi; Anope::string name; CommandInfo *help = source.service->FindCommand("generic/help"); if (Command::FindCommandFromService("chanserv/levels", bi, name) && help) source.Reply(_("\n" "Access levels can be configured via the \002{levels}\002 command. See \002{msg}{service} {help} {levels}\002 for more information."), "msg"_kw = Config->StrictPrivmsg, "service"_kw = bi->nick, "help"_kw = help->cname, "levels"_kw = name); if (help) source.Reply(_("\n" "The \002ADD\002 command adds \037mask\037 to the access list at level \037level\037.\n" "Use of this command requires the \002{change}\002 privilege on \037channel\037.\n" "\002{msg}{service} {help} {command} ADD\002 for more information.\n" "\n" "The \002DEL\002 command removes \037mask\037 from the access list.\n" "Use of this command requires the \002{change}\002 privilege on \037channel\037.\n" "\002{msg}{service} {help} {command} DEL\002 for more information.\n" "\n" "The \002LIST\002 and \002VIEW\002 commands both show the access list for \037channel\037, but \002VIEW\002 also shows who created the access entry, and when the user was last seen.\n" "Use of these commands requires the \002{list}\002 privilege on \037channel\037.\n" "\002{msg}{service} {help} {command} [LIST | VIEW]\002 for more information.\n" "\n" "The \002CLEAR\002 command clears the access list." "Use of this command requires the \002{founder}\002 privilege on \037channel\037.\n" "\002{msg}{service} {help} {command} CLEAR\002 for more information."), "msg"_kw = Config->StrictPrivmsg, "service"_kw = source.service->nick, "command"_kw = source.GetCommand(), "help"_kw = help->cname, "change"_kw = "ACCESS_CHANGE", "list"_kw = "ACCESS_LIST", "founder"_kw = "FOUNDER"); } return true; } void OnSyntaxError(CommandSource &source, const Anope::string &subcommand) override { if (subcommand.equals_ci("ADD")) { SubcommandSyntaxError(source, subcommand, _("\037mask\037 \037level\037")); } else if (subcommand.equals_ci("DEL")) { SubcommandSyntaxError(source, subcommand, _("{\037mask\037 | \037entry-num\037 | \037list\037}")); } else { Command::OnSyntaxError(source, subcommand); } } }; class CommandCSLevels : public Command { void DoSet(CommandSource &source, ChanServ::Channel *ci, const Anope::string &privilege, const Anope::string &levelstr) { int level; if (levelstr.empty()) { this->OnSyntaxError(source, "SET"); return; } if (levelstr.equals_ci("FOUNDER")) { level = ChanServ::ACCESS_FOUNDER; } else { try { level = convertTo(levelstr); } catch (const ConvertException &) { this->OnSyntaxError(source, "SET"); return; } } if (level <= ChanServ::ACCESS_INVALID || level > ChanServ::ACCESS_FOUNDER) { source.Reply(_("Level must be between \002{0}\002 and \002{1}\002 inclusive."), ChanServ::ACCESS_INVALID + 1, ChanServ::ACCESS_FOUNDER - 1); return; } ChanServ::Privilege *p = ChanServ::service ? ChanServ::service->FindPrivilege(privilege) : nullptr; if (p == NULL) { CommandInfo *help = source.service->FindCommand("generic/help"); if (help) source.Reply(_("There is no such privilege \002{0}\002. See \002{0}{1} {2} {3}\002 for a list of valid settings."), privilege, Config->StrictPrivmsg, source.service->nick, help->cname, source.GetCommand()); return; } logger.Command(source, ci, _("{source} used {command} on {channel} to set {0} to level {1}"), p->name, level); ci->SetLevel(p->name, level); EventManager::Get()->Dispatch(&Event::LevelChange::OnLevelChange, source, ci, p->name, level); if (level == ChanServ::ACCESS_FOUNDER) source.Reply(_("Level for privilege \002{0}\002 on channel \002{1}\002 changed to \002founder only\002."), p->name, ci->GetName()); else source.Reply(_("Level for privilege \002{0}\002 on channel \002{1}\002 changed to \002{3}\002."), p->name, ci->GetName(), level); } void DoDisable(CommandSource &source, ChanServ::Channel *ci, const Anope::string &privilege) { if (privilege.empty()) { this->OnSyntaxError(source, "DISABLE"); return; } /* Don't allow disabling of the founder level. It would be hard to change it back if you don't have access to use this command */ if (privilege.equals_ci("FOUNDER")) { source.Reply(_("You can not disable the founder privilege because it would be impossible to reenable it at a later time.")); return; } ChanServ::Privilege *p = ChanServ::service ? ChanServ::service->FindPrivilege(privilege) : nullptr; if (p == nullptr) { CommandInfo *help = source.service->FindCommand("generic/help"); if (help) source.Reply(_("There is no such privilege \002{0}\002. See \002{0}{1} {2} {3}\002 for a list of valid settings."), privilege, Config->StrictPrivmsg, source.service->nick, help->cname, source.GetCommand()); return; } logger.Command(source, ci, _("{source} used {command} on {channel} to disable {0}"), p->name); ci->SetLevel(p->name, ChanServ::ACCESS_INVALID); EventManager::Get()->Dispatch(&Event::LevelChange::OnLevelChange, source, ci, p->name, ChanServ::ACCESS_INVALID); source.Reply(_("Privilege \002{0}\002 disabled on channel \002{1}\002."), p->name, ci->GetName()); } void DoList(CommandSource &source, ChanServ::Channel *ci) { if (!ChanServ::service) return; source.Reply(_("Access level settings for channel \002{0}\002"), ci->GetName()); ListFormatter list(source.GetAccount()); list.AddColumn(_("Name")).AddColumn(_("Level")); const std::vector &privs = ChanServ::service->GetPrivileges(); for (unsigned i = 0; i < privs.size(); ++i) { const ChanServ::Privilege &p = privs[i]; int16_t j = ci->GetLevel(p.name); ListFormatter::ListEntry entry; entry["Name"] = p.name; if (j == ChanServ::ACCESS_INVALID) entry["Level"] = Language::Translate(source.GetAccount(), _("(disabled)")); else if (j == ChanServ::ACCESS_FOUNDER) entry["Level"] = Language::Translate(source.GetAccount(), _("(founder only)")); else entry["Level"] = stringify(j); list.AddEntry(entry); } std::vector replies; list.Process(replies); for (unsigned i = 0; i < replies.size(); ++i) source.Reply(replies[i]); } void DoReset(CommandSource &source, ChanServ::Channel *ci) { logger.Command(source, ci, _("{source} used {command} on {channel} to reset all levels")); ci->ClearLevels(); EventManager::Get()->Dispatch(&Event::LevelChange::OnLevelChange, source, ci, "ALL", 0); source.Reply(_("Levels for \002{0}\002 reset to defaults."), ci->GetName()); } public: CommandCSLevels(Module *creator) : Command(creator, "chanserv/levels", 2, 4) { this->SetDesc(_("Redefine the meanings of access levels")); this->SetSyntax(_("\037channel\037 SET \037privilege\037 \037level\037")); this->SetSyntax(_("\037channel\037 {DIS | DISABLE} \037privilege\037")); this->SetSyntax(_("\037channel\037 LIST")); this->SetSyntax(_("\037channel\037 RESET")); } void Execute(CommandSource &source, const std::vector ¶ms) override { const Anope::string &chan = params[0]; const Anope::string &cmd = params[1]; const Anope::string &privilege = params.size() > 2 ? params[2] : ""; const Anope::string &level = params.size() > 3 ? params[3] : ""; ChanServ::Channel *ci = ChanServ::Find(chan); if (ci == nullptr) { source.Reply(_("Channel \002{0}\002 isn't registered."), chan); return; } bool has_access = false; if (source.AccessFor(ci).HasPriv("FOUNDER")) has_access = true; else if (source.HasOverridePriv("chanserv/access/modify")) has_access = true; else if (cmd.equals_ci("LIST") && source.HasOverridePriv("chanserv/access/list")) has_access = true; if (!has_access) { source.Reply(_("Access denied. You do not have privilege \002{0}\002 on \002{1}\002."), "FOUNDER", ci->GetName()); return; } if (Anope::ReadOnly && !cmd.equals_ci("LIST")) { source.Reply(_("Services are in read-only mode.")); return; } if (cmd.equals_ci("SET")) this->DoSet(source, ci, privilege, level); else if (cmd.equals_ci("DIS") || cmd.equals_ci("DISABLE")) this->DoDisable(source, ci, privilege); else if (cmd.equals_ci("LIST")) this->DoList(source, ci); else if (cmd.equals_ci("RESET")) this->DoReset(source, ci); else this->OnSyntaxError(source, ""); } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { if (subcommand.equals_ci("DESC")) { source.Reply(_("The following privileges are available:")); ListFormatter list(source.GetAccount()); list.AddColumn(_("Name")).AddColumn(_("Description")); if (ChanServ::service) { const std::vector &privs = ChanServ::service->GetPrivileges(); for (unsigned i = 0; i < privs.size(); ++i) { const ChanServ::Privilege &p = privs[i]; ListFormatter::ListEntry entry; entry["Name"] = p.name; entry["Description"] = Language::Translate(source.nc, p.desc.c_str()); list.AddEntry(entry); } } std::vector replies; list.Process(replies); for (unsigned i = 0; i < replies.size(); ++i) source.Reply(replies[i]); } else { ServiceBot *bi; Anope::string name; if (!Command::FindCommandFromService("chanserv/access", bi, name) || bi != source.service) return false; CommandInfo *help = source.service->FindCommand("generic/help"); if (!help) return false; source.Reply(_("The \002{0}\002 command allows fine control over the meaning of numeric access levels used in the \002{1}\001 command.\n" "\n" "\002{0} SET\002 allows changing which \037privilege\037 is included in a given \037level\37.\n" "\n" "\002{0} DISABLE\002 disables a privilege and prevents anyone from be granted it, even channel founders." " The \002{2}\002 privilege can not be disabled.\n" "\n" "\002{0} LIST\002 shows the current level for each privilege.\n" "\n" "\002{0} RESET\002 resets the levels to the default levels for newly registered channels.\n" "\n" "For the list of privileges and their descriptions, see \002{3} {4} DESC\002."), source.GetCommand(), name, "FOUNDER", help->cname, source.GetCommand()); } return true; } void OnSyntaxError(CommandSource &source, const Anope::string &subcommand) override { if (subcommand.equals_ci("SET")) { SubcommandSyntaxError(source, subcommand, "\037privilege\037 \037level\037"); } else if (subcommand.equals_ci("DISABLE")) { SubcommandSyntaxError(source, subcommand, "\037privilege\037"); } else { Command::OnSyntaxError(source, subcommand); } } }; class CSAccess : public Module , public EventHook { CommandCSAccess commandcsaccess; CommandCSLevels commandcslevels; AccessChanAccessType accesschanaccesstype; public: CSAccess(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR) , EventHook(this) , commandcsaccess(this) , commandcslevels(this) , accesschanaccesstype(this) { this->SetPermanent(true); } EventReturn OnGroupCheckPriv(const ChanServ::AccessGroup *group, const Anope::string &priv) override { if (group->ci == NULL) return EVENT_CONTINUE; ChanServ::ChanAccess *highest = group->Highest(); if (highest && highest->GetSerializableType() == &accesschanaccesstype) { /* Access is the only access provider with the concept of negative access, * so check they don't have negative access */ AccessChanAccess *aca = anope_dynamic_static_cast(highest); if (aca->GetLevel() < 0) return EVENT_CONTINUE; } /* Special case. Allows a level of -1 to match anyone, and a level of 0 to match anyone identified. */ int16_t level = group->ci->GetLevel(priv); if (level == -1) return EVENT_ALLOW; else if (level == 0 && group->nc) return EVENT_ALLOW; return EVENT_CONTINUE; } }; template<> void ModuleInfo(ModuleDef *def) { def->Depends("chanserv"); } MODULE_INIT(CSAccess)