summaryrefslogtreecommitdiff
path: root/modules/chanserv/access.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'modules/chanserv/access.cpp')
-rw-r--r--modules/chanserv/access.cpp930
1 files changed, 930 insertions, 0 deletions
diff --git a/modules/chanserv/access.cpp b/modules/chanserv/access.cpp
new file mode 100644
index 000000000..c68af6e6f
--- /dev/null
+++ b/modules/chanserv/access.cpp
@@ -0,0 +1,930 @@
+/*
+ * Anope IRC Services
+ *
+ * Copyright (C) 2003-2017 Anope Team <team@anope.org>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+/* 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<int> level;
+
+ public:
+ static constexpr const char *NAME = "accesschanaccess";
+
+ using AccessChanAccess::AccessChanAccess;
+
+ int GetLevel();
+ void SetLevel(const int &);
+
+ 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<int>(data));
+ }
+ catch (const ConvertException &)
+ {
+ }
+ }
+
+ int Compare(ChanAccess *other) override
+ {
+ if (this->GetSerializableType() != other->GetSerializableType())
+ return ChanAccess::Compare(other);
+
+ int lev = this->GetLevel();
+ int theirlev = anope_dynamic_static_cast<AccessChanAccess *>(other)->GetLevel();
+
+ if (lev > theirlev)
+ return 1;
+ else if (lev < theirlev)
+ return -1;
+ else
+ return 0;
+ }
+};
+
+class AccessChanAccessType : public ChanAccessType<AccessChanAccessImpl>
+{
+ public:
+ Serialize::Field<AccessChanAccessImpl, int> level;
+
+ AccessChanAccessType(Module *me) : ChanAccessType<AccessChanAccessImpl>(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<int>(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<AccessChanAccess *>();
+ 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<bool>("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 *access = ci->GetAccess(i - 1);
+ if (mask.equals_ci(access->Mask()))
+ {
+ /* Don't allow lowering from a level >= u_level */
+ if ((!highest || *access >= *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(), access->Mask());
+ return;
+ }
+ access->Delete();
+ break;
+ }
+ }
+
+ unsigned access_max = Config->GetModule("chanserv/main")->Get<unsigned>("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<AccessChanAccess *>();
+ 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 (ci->c)
+ for (Channel::ChanUserList::const_iterator cit = ci->c->users.begin(), cit_end = ci->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 (ci->c)
+ for (Channel::ChanUserList::const_iterator cit = ci->c->users.begin(), cit_end = ci->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<Anope::string> 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());
+
+ bool override = !source.IsFounder(ci);
+ 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<Anope::string> &params) 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_clear = cmd.equals_ci("CLEAR");
+ 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;
+ }
+};
+
+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<int>(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<ChanServ::Privilege> &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<Anope::string> 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<Anope::string> &params) 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<ChanServ::Privilege> &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<Anope::string> 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;
+ }
+};
+
+class CSAccess : public Module
+ , public EventHook<Event::GroupCheckPriv>
+{
+ CommandCSAccess commandcsaccess;
+ CommandCSLevels commandcslevels;
+ AccessChanAccessType accesschanaccesstype;
+
+ public:
+ CSAccess(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR)
+ , EventHook<Event::GroupCheckPriv>(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<AccessChanAccess *>(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<CSAccess>(ModuleDef *def)
+{
+ def->Depends("chanserv");
+}
+
+MODULE_INIT(CSAccess)