From 6302f980fe6dad83aab7d2dc1046dadea5ffd939 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 14 Dec 2015 16:52:24 -0500 Subject: New source tree structure for modules. From commands/cs_access => chanserv/access etc. --- modules/chanserv/CMakeLists.txt | 1 + modules/chanserv/akick.cpp | 718 ++++++++++++++++++ modules/chanserv/ban.cpp | 268 +++++++ modules/chanserv/clone.cpp | 221 ++++++ modules/chanserv/cs_access.old | 941 ++++++++++++++++++++++++ modules/chanserv/cs_flags.old | 528 +++++++++++++ modules/chanserv/cs_xop.old | 659 +++++++++++++++++ modules/chanserv/drop.cpp | 101 +++ modules/chanserv/enforce.cpp | 307 ++++++++ modules/chanserv/entrymsg.cpp | 270 +++++++ modules/chanserv/getkey.cpp | 75 ++ modules/chanserv/info.cpp | 97 +++ modules/chanserv/invite.cpp | 109 +++ modules/chanserv/kick.cpp | 152 ++++ modules/chanserv/list.cpp | 263 +++++++ modules/chanserv/log.cpp | 448 +++++++++++ modules/chanserv/main/CMakeLists.txt | 1 + modules/chanserv/main/chanaccess.cpp | 127 ++++ modules/chanserv/main/chanaccess.h | 30 + modules/chanserv/main/chanaccesstype.h | 21 + modules/chanserv/main/channel.cpp | 429 +++++++++++ modules/chanserv/main/channel.h | 68 ++ modules/chanserv/main/channeltype.cpp | 45 ++ modules/chanserv/main/channeltype.h | 34 + modules/chanserv/main/chanserv.cpp | 587 +++++++++++++++ modules/chanserv/main/level.cpp | 33 + modules/chanserv/main/level.h | 16 + modules/chanserv/main/leveltype.h | 16 + modules/chanserv/main/mode.cpp | 33 + modules/chanserv/main/mode.h | 17 + modules/chanserv/main/modetype.h | 15 + modules/chanserv/mode.cpp | 999 +++++++++++++++++++++++++ modules/chanserv/register.cpp | 157 ++++ modules/chanserv/seen.cpp | 477 ++++++++++++ modules/chanserv/set.cpp | 1265 ++++++++++++++++++++++++++++++++ modules/chanserv/set_misc.cpp | 211 ++++++ modules/chanserv/status.cpp | 115 +++ modules/chanserv/statusupdate.cpp | 67 ++ modules/chanserv/suspend.cpp | 339 +++++++++ modules/chanserv/sync.cpp | 77 ++ modules/chanserv/topic.cpp | 276 +++++++ modules/chanserv/unban.cpp | 120 +++ modules/chanserv/updown.cpp | 261 +++++++ 43 files changed, 10994 insertions(+) create mode 100644 modules/chanserv/CMakeLists.txt create mode 100644 modules/chanserv/akick.cpp create mode 100644 modules/chanserv/ban.cpp create mode 100644 modules/chanserv/clone.cpp create mode 100644 modules/chanserv/cs_access.old create mode 100644 modules/chanserv/cs_flags.old create mode 100644 modules/chanserv/cs_xop.old create mode 100644 modules/chanserv/drop.cpp create mode 100644 modules/chanserv/enforce.cpp create mode 100644 modules/chanserv/entrymsg.cpp create mode 100644 modules/chanserv/getkey.cpp create mode 100644 modules/chanserv/info.cpp create mode 100644 modules/chanserv/invite.cpp create mode 100644 modules/chanserv/kick.cpp create mode 100644 modules/chanserv/list.cpp create mode 100644 modules/chanserv/log.cpp create mode 100644 modules/chanserv/main/CMakeLists.txt create mode 100644 modules/chanserv/main/chanaccess.cpp create mode 100644 modules/chanserv/main/chanaccess.h create mode 100644 modules/chanserv/main/chanaccesstype.h create mode 100644 modules/chanserv/main/channel.cpp create mode 100644 modules/chanserv/main/channel.h create mode 100644 modules/chanserv/main/channeltype.cpp create mode 100644 modules/chanserv/main/channeltype.h create mode 100644 modules/chanserv/main/chanserv.cpp create mode 100644 modules/chanserv/main/level.cpp create mode 100644 modules/chanserv/main/level.h create mode 100644 modules/chanserv/main/leveltype.h create mode 100644 modules/chanserv/main/mode.cpp create mode 100644 modules/chanserv/main/mode.h create mode 100644 modules/chanserv/main/modetype.h create mode 100644 modules/chanserv/mode.cpp create mode 100644 modules/chanserv/register.cpp create mode 100644 modules/chanserv/seen.cpp create mode 100644 modules/chanserv/set.cpp create mode 100644 modules/chanserv/set_misc.cpp create mode 100644 modules/chanserv/status.cpp create mode 100644 modules/chanserv/statusupdate.cpp create mode 100644 modules/chanserv/suspend.cpp create mode 100644 modules/chanserv/sync.cpp create mode 100644 modules/chanserv/topic.cpp create mode 100644 modules/chanserv/unban.cpp create mode 100644 modules/chanserv/updown.cpp (limited to 'modules/chanserv') diff --git a/modules/chanserv/CMakeLists.txt b/modules/chanserv/CMakeLists.txt new file mode 100644 index 000000000..cd225a94d --- /dev/null +++ b/modules/chanserv/CMakeLists.txt @@ -0,0 +1 @@ +build_modules(${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/modules/chanserv/akick.cpp b/modules/chanserv/akick.cpp new file mode 100644 index 000000000..8cd1f778a --- /dev/null +++ b/modules/chanserv/akick.cpp @@ -0,0 +1,718 @@ +/* ChanServ core functions + * + * (C) 2003-2014 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_akick.h" + +class AutoKickImpl : public AutoKick +{ + public: + AutoKickImpl(Serialize::TypeBase *type) : AutoKick(type) { } + AutoKickImpl(Serialize::TypeBase *type, Serialize::ID id) : AutoKick(type, id) { } + + ChanServ::Channel *GetChannel() override; + void SetChannel(ChanServ::Channel *ci) override; + + Anope::string GetMask() override; + void SetMask(const Anope::string &mask) override; + + NickServ::Account *GetAccount() override; + void SetAccount(NickServ::Account *nc) override; + + Anope::string GetReason() override; + void SetReason(const Anope::string &r) override; + + Anope::string GetCreator() override; + void SetCreator(const Anope::string &c) override; + + time_t GetAddTime() override; + void SetAddTime(const time_t &t) override; + + time_t GetLastUsed() override; + void SetLastUsed(const time_t &t) override; +}; + +class AutoKickType : public Serialize::Type +{ + public: + Serialize::ObjectField ci; + + Serialize::Field mask; + Serialize::ObjectField nc; + + Serialize::Field reason; + Serialize::Field creator; + Serialize::Field addtime; + Serialize::Field last_time; + + AutoKickType(Module *me) + : Serialize::Type(me, "AutoKick") + , ci(this, "ci", true) + , mask(this, "mask") + , nc(this, "nc", true) + , reason(this, "reason") + , creator(this, "creator") + , addtime(this, "addtime") + , last_time(this, "last_time") + { + } +}; + +ChanServ::Channel *AutoKickImpl::GetChannel() +{ + return Get(&AutoKickType::ci); +} + +void AutoKickImpl::SetChannel(ChanServ::Channel *ci) +{ + Set(&AutoKickType::ci, ci); +} + +Anope::string AutoKickImpl::GetMask() +{ + return Get(&AutoKickType::mask); +} + +void AutoKickImpl::SetMask(const Anope::string &mask) +{ + Set(&AutoKickType::mask, mask); +} + +NickServ::Account *AutoKickImpl::GetAccount() +{ + return Get(&AutoKickType::nc); +} + +void AutoKickImpl::SetAccount(NickServ::Account *nc) +{ + Set(&AutoKickType::nc, nc); +} + +Anope::string AutoKickImpl::GetReason() +{ + return Get(&AutoKickType::reason); +} + +void AutoKickImpl::SetReason(const Anope::string &r) +{ + Set(&AutoKickType::reason, r); +} + +Anope::string AutoKickImpl::GetCreator() +{ + return Get(&AutoKickType::creator); +} + +void AutoKickImpl::SetCreator(const Anope::string &c) +{ + Set(&AutoKickType::creator, c); +} + +time_t AutoKickImpl::GetAddTime() +{ + return Get(&AutoKickType::addtime); +} + +void AutoKickImpl::SetAddTime(const time_t &t) +{ + Set(&AutoKickType::addtime, t); +} + +time_t AutoKickImpl::GetLastUsed() +{ + return Get(&AutoKickType::last_time); +} + +void AutoKickImpl::SetLastUsed(const time_t &t) +{ + Set(&AutoKickType::last_time, t); +} + +class CommandCSAKick : public Command +{ + void DoAdd(CommandSource &source, ChanServ::Channel *ci, const std::vector ¶ms) + { + Anope::string mask = params[2]; + Anope::string reason = params.size() > 3 ? params[3] : ""; + NickServ::Nick *na = NickServ::FindNick(mask); + NickServ::Account *nc = NULL; + unsigned reasonmax = Config->GetModule("chanserv")->Get("reasonmax", "200"); + + if (reason.length() > reasonmax) + reason = reason.substr(0, reasonmax); + + if (IRCD->IsExtbanValid(mask)) + ; /* If this is an extban don't try to complete the mask */ + else if (IRCD->IsChannelValid(mask)) + { + /* Also don't try to complete the mask if this is a channel */ + + if (mask.equals_ci(ci->GetName()) && ci->HasFieldS("PEACE")) + { + source.Reply(_("Access denied.")); + return; + } + } + else if (!na) + { + /* If the mask contains a realname the reason must be prepended with a : */ + if (mask.find('#') != Anope::string::npos) + { + size_t r = reason.find(':'); + if (r != Anope::string::npos) + { + mask += " " + reason.substr(0, r); + mask.trim(); + reason = reason.substr(r + 1); + reason.trim(); + } + else + { + mask = mask + " " + reason; + reason.clear(); + } + } + + Entry e("", mask); + + mask = (e.nick.empty() ? "*" : e.nick) + "!" + + (e.user.empty() ? "*" : e.user) + "@" + + (e.host.empty() ? "*" : e.host); + if (!e.real.empty()) + mask += "#" + e.real; + } + else + nc = na->GetAccount(); + + /* Check excepts BEFORE we get this far */ + if (ci->c) + { + std::vector modes = ci->c->GetModeList("EXCEPT"); + for (unsigned int i = 0; i < modes.size(); ++i) + { + if (Anope::Match(modes[i], mask)) + { + source.Reply(_("\002{0}\002 matches an except on \002{1}\002 and cannot be banned until the except has been removed."), mask, ci->GetName()); + return; + } + } + } + + bool override = !source.AccessFor(ci).HasPriv("AKICK"); + /* Opers overriding get to bypass PEACE */ + if (override) + ; + /* These peace checks are only for masks */ + else if (IRCD->IsChannelValid(mask)) + ; + /* Check whether target nick has equal/higher access + * or whether the mask matches a user with higher/equal access - Viper */ + else if (ci->HasFieldS("PEACE") && nc) + { + ChanServ::AccessGroup nc_access = ci->AccessFor(nc), u_access = source.AccessFor(ci); + if (nc == ci->GetFounder() || nc_access >= u_access) + { + source.Reply(_("Access denied. You can not auto kick \002{0}\002 because they have more privileges than you on \002{1}\002 and \002{2}\002 is enabled."), nc->GetDisplay(), ci->GetName(), "PEACE"); + return; + } + } + else if (ci->HasFieldS("PEACE")) + { +#if 0 + /* Match against all currently online users with equal or + * higher access. - Viper */ + for (user_map::const_iterator it = UserListByNick.begin(); it != UserListByNick.end(); ++it) + { + User *u2 = it->second; + + ChanServ::AccessGroup nc_access = ci->AccessFor(nc), u_access = source.AccessFor(ci); + Entry entry_mask("", mask); + + if ((ci->AccessFor(u2).HasPriv("FOUNDER") || nc_access >= u_access) && entry_mask.Matches(u2)) + { + source.Reply(ACCESS_DENIED); + return; + } + } + + /* Match against the lastusermask of all nickalias's with equal + * or higher access. - Viper */ + for (nickalias_map::const_iterator it = NickServ::NickList->begin(), it_end = NickServ::NickList->end(); it != it_end; ++it) + { + na = it->second; + + ChanServ::AccessGroup nc_access = ci->AccessFor(na->GetAccount()), u_access = source.AccessFor(ci); + if (na->GetAccount() && (na->GetAccount() == ci->GetFounder() || nc_access >= u_access)) + { + Anope::string buf = na->GetNick() + "!" + na->GetLastUsermask(); + if (Anope::Match(buf, mask)) + { + source.Reply(ACCESS_DENIED); + return; + } + } + } +#endif + } + + for (unsigned j = 0, end = ci->GetAkickCount(); j < end; ++j) + { + AutoKick *ak = ci->GetAkick(j); + if (ak->GetAccount() ? ak->GetAccount() == nc : mask.equals_ci(ak->GetMask())) + { + source.Reply(_("\002{0}\002 already exists on \002{1}\002 autokick list."), ak->GetAccount() ? ak->GetAccount()->GetDisplay() : ak->GetMask(), ci->GetName()); + return; + } + } + + if (ci->GetAkickCount() >= Config->GetModule(this->owner)->Get("autokickmax")) + { + source.Reply(_("Sorry, you can only have \002{0}\002 autokick masks on a channel."), Config->GetModule(this->owner)->Get("autokickmax")); + return; + } + + AutoKick *ak; + if (nc) + ak = ci->AddAkick(source.GetNick(), nc, reason); + else + ak = ci->AddAkick(source.GetNick(), mask, reason); + + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to add " << mask << (reason == "" ? "" : ": ") << reason; + + this->akickevents(&Event::Akick::OnAkickAdd, source, ci, ak); + + source.Reply(_("\002{0}\002 added to \002{1}\002 autokick list."), mask, ci->GetName()); + + this->DoEnforce(source, ci); + } + + void DoDel(CommandSource &source, ChanServ::Channel *ci, const std::vector ¶ms) + { + const Anope::string &mask = params[2]; + bool override = !source.AccessFor(ci).HasPriv("AKICK"); + + if (!ci->GetAkickCount()) + { + source.Reply(_("The autokick list of \002{0}\002 is empty."), ci->GetName()); + return; + } + + /* Special case: is it a number/list? Only do search if it isn't. */ + if (isdigit(mask[0]) && mask.find_first_not_of("1234567890,-") == Anope::string::npos) + { + unsigned int deleted = 0; + + NumberList(mask, true, + [&](unsigned int number) + { + if (!number || number > ci->GetAkickCount()) + return; + + AutoKick *ak = ci->GetAkick(number - 1); + + this->akickevents(&Event::Akick::OnAkickDel, source, ci, ak); + + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to delete " << (ak->GetAccount() ? ak->GetAccount()->GetDisplay() : ak->GetMask()); + + ++deleted; + delete ak; + }, + [&]() + { + if (!deleted) + source.Reply(_("There are no entries matching \002{0}\002 on the auto kick list of \002{1}\002."), + mask, ci->GetName()); + else if (deleted == 1) + source.Reply(_("Deleted \0021\002 entry from the autokick list of \002{0}\002."), ci->GetName()); + else + source.Reply(_("Deleted \002{0}\002 entries from \002{1}\002 autokick list."), deleted, ci->GetName()); + }); + } + else + { + NickServ::Nick *na = NickServ::FindNick(mask); + NickServ::Account *nc = na ? na->GetAccount() : nullptr; + + unsigned int i, end; + for (i = 0, end = ci->GetAkickCount(); i < end; ++i) + { + AutoKick *ak = ci->GetAkick(i); + + if (ak->GetAccount() ? ak->GetAccount() == nc : mask.equals_ci(ak->GetMask())) + break; + } + + if (i == ci->GetAkickCount()) + { + source.Reply(_("\002{0}\002 was not found on the auto kick list of \002{1}\002."), mask, ci->GetName()); + return; + } + + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to delete " << mask; + + this->akickevents(&Event::Akick::OnAkickDel, source, ci, ci->GetAkick(i)); + + delete ci->GetAkick(i); + + source.Reply(_("\002{0}\002 deleted from the auto kick list of \002{1}\002."), mask, ci->GetName()); + } + } + + void ProcessList(CommandSource &source, ChanServ::Channel *ci, const std::vector ¶ms, ListFormatter &list) + { + const Anope::string &mask = params.size() > 2 ? params[2] : ""; + + if (!mask.empty() && isdigit(mask[0]) && mask.find_first_not_of("1234567890,-") == Anope::string::npos) + { + NumberList(mask, false, + [&](unsigned int number) + { + if (!number || number > ci->GetAkickCount()) + return; + + AutoKick *ak = ci->GetAkick(number - 1); + + Anope::string timebuf, lastused; + if (ak->GetAddTime()) + timebuf = Anope::strftime(ak->GetAddTime(), NULL, true); + else + timebuf = _(""); + if (ak->GetLastUsed()) + lastused = Anope::strftime(ak->GetLastUsed(), NULL, true); + else + lastused = _(""); + + ListFormatter::ListEntry entry; + entry["Number"] = stringify(number); + if (ak->GetAccount()) + entry["Mask"] = ak->GetAccount()->GetDisplay(); + else + entry["Mask"] = ak->GetMask(); + entry["Creator"] = ak->GetCreator(); + entry["Created"] = timebuf; + entry["Last used"] = ak->GetLastUsed(); + entry["Reason"] = ak->GetReason(); + list.AddEntry(entry); + }, + []{}); + } + else + { + for (unsigned i = 0, end = ci->GetAkickCount(); i < end; ++i) + { + AutoKick *ak = ci->GetAkick(i); + + if (!mask.empty()) + { + if (!ak->GetAccount() && !Anope::Match(ak->GetMask(), mask)) + continue; + if (ak->GetAccount() && !Anope::Match(ak->GetAccount()->GetDisplay(), mask)) + continue; + } + + Anope::string timebuf, lastused; + if (ak->GetAddTime()) + timebuf = Anope::strftime(ak->GetAddTime(), NULL, true); + else + timebuf = _(""); + if (ak->GetLastUsed()) + lastused = Anope::strftime(ak->GetLastUsed(), NULL, true); + else + lastused = _(""); + + ListFormatter::ListEntry entry; + entry["Number"] = stringify(i + 1); + if (ak->GetAccount()) + entry["Mask"] = ak->GetAccount()->GetDisplay(); + else + entry["Mask"] = ak->GetMask(); + entry["Creator"] = ak->GetCreator(); + entry["Created"] = timebuf; + entry["Last used"] = lastused; + entry["Reason"] = ak->GetReason(); + list.AddEntry(entry); + } + } + + if (list.IsEmpty()) + { + source.Reply(_("There are no entries matching \002{0}\002 on the autokick list of \002{1}\002."), mask, ci->GetName()); + return; + } + + std::vector replies; + list.Process(replies); + + source.Reply(_("Autokick list for \002{0}\002:"), ci->GetName()); + + for (unsigned i = 0; i < replies.size(); ++i) + source.Reply(replies[i]); + + source.Reply(_("End of autokick list.")); + } + + void DoList(CommandSource &source, ChanServ::Channel *ci, const std::vector ¶ms) + { + if (!ci->GetAkickCount()) + { + source.Reply(_("The autokick list of \002{0}\002 is empty."), ci->GetName()); + return; + } + + ListFormatter list(source.GetAccount()); + list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("Reason")); + this->ProcessList(source, ci, params, list); + } + + void DoView(CommandSource &source, ChanServ::Channel *ci, const std::vector ¶ms) + { + if (!ci->GetAkickCount()) + { + source.Reply(_("The autokick list of \002{0}\002 is empty."), ci->GetName()); + return; + } + + ListFormatter list(source.GetAccount()); + list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("Creator")).AddColumn(_("Created")).AddColumn(_("Last used")).AddColumn(_("Reason")); + this->ProcessList(source, ci, params, list); + } + + void DoEnforce(CommandSource &source, ChanServ::Channel *ci) + { + Channel *c = ci->c; + int count = 0; + + if (!c) + { + source.Reply(_("Channel \002{0}\002 doesn't exist."), ci->GetName()); + return; + } + + for (Channel::ChanUserList::iterator it = c->users.begin(), it_end = c->users.end(); it != it_end; ) + { + ChanUserContainer *uc = it->second; + ++it; + + if (c->CheckKick(uc->user)) + ++count; + } + + bool override = !source.AccessFor(ci).HasPriv("AKICK"); + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "ENFORCE, affects " << count << " users"; + + source.Reply(_("Autokick enforce for \002{0}\002 complete; \002{1}\002 users were affected."), ci->GetName(), count); + } + + void DoClear(CommandSource &source, ChanServ::Channel *ci) + { + bool override = !source.AccessFor(ci).HasPriv("AKICK"); + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to clear the akick list"; + + ci->ClearAkick(); + source.Reply(_("The autokick list of \002{0}\002 has been cleared."), ci->GetName()); + } + + EventHandlers &akickevents; + + public: + CommandCSAKick(Module *creator, EventHandlers &events) : Command(creator, "chanserv/akick", 2, 4), akickevents(events) + { + this->SetDesc(_("Maintain the AutoKick list")); + this->SetSyntax(_("\037channel\037 ADD {\037nick\037 | \037mask\037} [\037reason\037]")); + this->SetSyntax(_("\037channel\037 DEL {\037nick\037 | \037mask\037 | \037entry-num\037 | \037list\037}")); + this->SetSyntax(_("\037channel\037 LIST [\037mask\037 | \037entry-num\037 | \037list\037]")); + this->SetSyntax(_("\037channel\037 VIEW [\037mask\037 | \037entry-num\037 | \037list\037]")); + this->SetSyntax(_("\037channel\037 ENFORCE")); + 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 &mask = params.size() > 2 ? params[2] : ""; + + ChanServ::Channel *ci = ChanServ::Find(chan); + if (ci == NULL) + { + source.Reply(_("Channel \002{0}\002 isn't registered."), chan); + return; + } + + bool is_list = cmd.equals_ci("LIST") || cmd.equals_ci("VIEW"); + + if (mask.empty() && (cmd.equals_ci("ADD") || cmd.equals_ci("DEL"))) + { + this->OnSyntaxError(source, cmd); + return; + } + + if (!source.AccessFor(ci).HasPriv("AKICK") && !source.HasPriv("chanserv/access/modify") && (!is_list || source.HasPriv("chanserv/access/list"))) + { + source.Reply(_("Access denied. You do not have privilege \002{0}\002 on \002{1}\002."), "AKICK", ci->GetName()); + return; + } + + if (Anope::ReadOnly && (cmd.equals_ci("ADD") || cmd.equals_ci("DEL") || cmd.equals_ci("CLEAR"))) + { + source.Reply(_("Sorry, channel autokick list modification is temporarily disabled.")); + return; + } + + if (cmd.equals_ci("ADD")) + this->DoAdd(source, ci, params); + else if (cmd.equals_ci("DEL")) + this->DoDel(source, ci, params); + else if (cmd.equals_ci("LIST")) + this->DoList(source, ci, params); + else if (cmd.equals_ci("VIEW")) + this->DoView(source, ci, params); + else if (cmd.equals_ci("ENFORCE")) + this->DoEnforce(source, ci); + 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 command adds \037mask\037 to the auto kick list list of \037channel\037." + " If \037reason\037 is given, it is used when the user is kicked." + " If \037mask\037 is an account, then all users using the account will be affected.\n" + "\n" + "Examples:\n" + " {command} #anope ADD Cronus noob!\n" + " Adds the account \"Cronus\" to the auto kick list of \"#anope\" with the reason \"noob!\".\n" + "\n" + " {command} #anope ADD Guest*!*@*\n" + " Adds the mask \"Guest*!*@*\" to the auto kick list of \"#anope\"."), + source.command); + else if (subcommand.equals_ci("DEL")) + source.Reply(_("The \002{0} DEL\002 command removes \037mask\037 from the auto kick list." + " It does not, however, remove any bans placed by an auto kick; those must be removed manually.\n" + "\n" + "Example:\n" + " {command} #anope DEL Cronus!\n" + " Removes the auto kick for \"Cronus\" from the auto kick list of \"#anope\".\n"), + source.command); + 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 auto kick 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 auto kick entry, when it was created, and when it was last used." + "\n" + "Example:\n" + " \002{0} #anope LIST 2-5,7-9\002\n" + " Lists auto kick entries numbered 2 through 5 and 7 through 9 on #anope."), + source.command); + else if (subcommand.equals_ci("ENFORCE")) + source.Reply(_("The \002{0} ENFORCE\002 command enforces the auto kick list by forcibly removing users who match an entry on the auto kick list." + "This can be useful if someone does not authenticate or change their host mask until after joining.\n" + "\n" + "Example:\n" + " \002{0} #anope ENFORCE\002\n" + " Enforces the auto kick list of #anope."), + source.command); + else if (subcommand.equals_ci("CLEAR")) + source.Reply(_("The \002{0} CLEAR\002 command clears the auto kick list of \037channel\37. As with the \002DEL\002 command, existing bans placed by auto kick will not be removed.\n" + "\n" + "Example:\n" + " \002{0} #anope CLEAR\002\n" + " Clears the auto kick list of #anope."), + source.command); + else + { + source.Reply(_("Maintains the auto kick list for \037channel\037." + " If a user matching an entry on the auto kick list joins the channel, {0} will place a ban on the user and kick them.\n" + "\n" + "Use of this command requires the \002{1}\002 privilege on \037channel\037."), + source.service->nick, "AKICK"); + CommandInfo *help = source.service->FindCommand("generic/help"); + if (help) + source.Reply(_("\n" + "The \002ADD\002 command adds \037nick\037 or \037mask\037 to the auto kick list of \037channel\037.\n" + "\002{msg}{service} {help} {command} ADD\002 for more information.\n" + "\n" + "The \002DEL\002 command removes \037mask\037 from the auto kick list of \037channel\037.\n" + "\002{msg}{service} {help} {command} DEL\002 for more information.\n" + "\n" + "The \002LIST\002 and \002VIEW\002 commands both display the auto kick list of \037channel\037, but \002VIEW\002 also displays who created the auto kick entry, when it was created, and when it was last used.\n" + "\002{msg}{service} {help} {command} [LIST | VIEW]\002 for more information.\n" + "\n" + "The \002ENFORCE\002 command enforces the auto kick list by forcibly removing users who match an entry on the auto kick list.\n" + "\002{msg}{service} {help} {command} ENFORCE\002 for more information.\n" + "" + "The \002CLEAR\002 clears the auto kick list of \037channel\037." + "\002{msg}{service} {help} {command} CLEAR\002 for more information.\n"), + "msg"_kw = Config->StrictPrivmsg, "service"_kw = source.service->nick, "help"_kw = help->cname, "command"_kw = source.command); + } + + return true; + } +}; + +class CSAKick : public Module + , public EventHook +{ + CommandCSAKick commandcsakick; + EventHandlers akickevents; + AutoKickType akick_type; + + public: + CSAKick(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR) + , commandcsakick(this, akickevents) + , akickevents(this) + , akick_type(this) + { + } + + EventReturn OnCheckKick(User *u, Channel *c, Anope::string &mask, Anope::string &reason) override + { + if (!c->ci || c->MatchesList(u, "EXCEPT")) + return EVENT_CONTINUE; + + for (unsigned j = 0, end = c->ci->GetAkickCount(); j < end; ++j) + { + AutoKick *ak = c->ci->GetAkick(j); + bool kick = false; + + if (ak->GetAccount()) + kick = ak->GetAccount() == u->Account(); + else if (IRCD->IsChannelValid(ak->GetMask())) + { + Channel *chan = Channel::Find(ak->GetMask()); + kick = chan != NULL && chan->FindUser(u); + } + else + kick = Entry("BAN", ak->GetMask()).Matches(u); + + if (kick) + { + Log(LOG_DEBUG_2) << u->nick << " matched akick " << (ak->GetAccount() ? ak->GetAccount()->GetDisplay() : ak->GetMask()); + ak->SetLastUsed(Anope::CurTime); + if (!ak->GetAccount() && ak->GetMask().find('#') == Anope::string::npos) + mask = ak->GetMask(); + reason = ak->GetReason(); + if (reason.empty()) + reason = Language::Translate(u, Config->GetModule(this)->Get("autokickreason").c_str()); + if (reason.empty()) + reason = Language::Translate(u, _("User has been banned from the channel")); + return EVENT_STOP; + } + } + + return EVENT_CONTINUE; + } +}; + +MODULE_INIT(CSAKick) diff --git a/modules/chanserv/ban.cpp b/modules/chanserv/ban.cpp new file mode 100644 index 000000000..f797eda0f --- /dev/null +++ b/modules/chanserv/ban.cpp @@ -0,0 +1,268 @@ +/* ChanServ core functions + * + * (C) 2003-2014 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 Module *me; + +class TempBan : public Timer +{ + private: + Anope::string channel; + Anope::string mask; + Anope::string mode; + + public: + TempBan(time_t seconds, Channel *c, const Anope::string &banmask, const Anope::string &mod) : Timer(me, seconds), channel(c->name), mask(banmask), mode(mod) { } + + void Tick(time_t ctime) override + { + Channel *c = Channel::Find(this->channel); + if (c) + c->RemoveMode(NULL, mode, this->mask); + } +}; + +class CommandCSBan : public Command +{ + public: + CommandCSBan(Module *creator) : Command(creator, "chanserv/ban", 2, 4) + { + this->SetDesc(_("Bans a given nick or mask on a channel")); + this->SetSyntax(_("\037channel\037 [+\037expiry\037] {\037nick\037 | \037mask\037} [\037reason\037]")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + const Anope::string &chan = params[0]; + Configuration::Block *block = Config->GetCommand(source); + const Anope::string &mode = block->Get("mode", "BAN"); + + ChanServ::Channel *ci = ChanServ::Find(chan); + if (ci == NULL) + { + source.Reply(_("Channel \002{0}\002 isn't registered."), chan); + return; + } + + Channel *c = ci->c; + if (c == NULL) + { + source.Reply(_("Channel \002{0}\002 doesn't exist."), ci->GetName()); + return; + } + + if (IRCD->GetMaxListFor(c) && c->HasMode(mode) >= IRCD->GetMaxListFor(c)) + { + source.Reply(_("The %s list for %s is full."), mode.lower().c_str(), c->name.c_str()); + return; + } + + Anope::string expiry, target, reason; + time_t ban_time; + if (params[1][0] == '+') + { + ban_time = Anope::DoTime(params[1]); + if (ban_time == -1) + { + source.Reply(_("Invalid expiry time \002{0}\002."), params[1]); + return; + } + if (params.size() < 3) + { + this->SendSyntax(source); + return; + } + target = params[2]; + reason = "Requested"; + if (params.size() > 3) + reason = params[3]; + } + else + { + ban_time = 0; + target = params[1]; + reason = "Requested"; + if (params.size() > 2) + reason = params[2]; + if (params.size() > 3) + reason += " " + params[3]; + } + + unsigned reasonmax = Config->GetModule("chanserv")->Get("reasonmax", "200"); + if (reason.length() > reasonmax) + reason = reason.substr(0, reasonmax); + + Anope::string signkickformat = Config->GetModule("chanserv")->Get("signkickformat", "%m (%n)"); + signkickformat = signkickformat.replace_all_cs("%n", source.GetNick()); + + User *u = source.GetUser(); + User *u2 = User::Find(target, true); + + ChanServ::AccessGroup u_access = source.AccessFor(ci); + + if (!u_access.HasPriv("BAN") && !source.HasPriv("chanserv/kick")) + { + source.Reply(_("Access denied. You do not have privilege \002{0}\002 on \002{1}\002."), "BAN", ci->GetName()); + return; + } + + if (u2) + { + ChanServ::AccessGroup u2_access = ci->AccessFor(u2); + + if (u != u2 && ci->HasFieldS("PEACE") && u2_access >= u_access && !source.HasPriv("chanserv/kick")) + { + source.Reply(_("Access denied. \002{0}\002 has the same or more privileges than you on \002{1}\002."), u2->nick, ci->GetName()); + return; + } + + /* + * Don't ban/kick the user on channels where he is excepted + * to prevent services <-> server wars. + */ + if (c->MatchesList(u2, "EXCEPT")) + { + source.Reply(_("\002{0}\002 matches an except on \002{1}\002 and can not be banned until the except has been removed."), u2->nick, ci->GetName()); + return; + } + + if (u2->IsProtected()) + { + source.Reply(_("Access denied. \002{0}\002 is protected and can not be kicked."), u2->nick); + return; + } + + Anope::string mask = ci->GetIdealBan(u2); + + bool override = !u_access.HasPriv("BAN") || (u != u2 && ci->HasFieldS("PEACE") && u2_access >= u_access); + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "for " << mask; + + if (!c->HasMode(mode, mask)) + { + c->SetMode(NULL, mode, mask); + if (ban_time) + { + new TempBan(ban_time, c, mask, mode); + source.Reply(_("Ban on \002{0}\002 expires in \002{1}\002."), mask, Anope::Duration(ban_time, source.GetAccount())); + } + } + + /* We still allow host banning while not allowing to kick */ + if (!c->FindUser(u2)) + return; + + if (block->Get("kick", "yes")) + { + if (ci->HasFieldS("SIGNKICK") || (ci->HasFieldS("SIGNKICK_LEVEL") && !source.AccessFor(ci).HasPriv("SIGNKICK"))) + { + signkickformat = signkickformat.replace_all_cs("%m", reason); + c->Kick(ci->WhoSends(), u2, "%s", signkickformat.c_str()); + } + else + c->Kick(ci->WhoSends(), u2, "%s", reason.c_str()); + } + } + else + { + bool founder = u_access.HasPriv("FOUNDER"); + bool override = !founder && !u_access.HasPriv("BAN"); + + Anope::string mask = IRCD->NormalizeMask(target); + + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "for " << mask; + + if (!c->HasMode(mode, mask)) + { + c->SetMode(NULL, mode, mask); + if (ban_time) + { + new TempBan(ban_time, c, mask, mode); + source.Reply(_("Ban on \002{0}\002 expires in \002{1}\002."), mask, Anope::Duration(ban_time, source.GetAccount())); + } + } + + int matched = 0, kicked = 0; + for (Channel::ChanUserList::iterator it = c->users.begin(), it_end = c->users.end(); it != it_end;) + { + ChanUserContainer *uc = it->second; + ++it; + + Entry e(mode, mask); + if (e.Matches(uc->user)) + { + ++matched; + + ChanServ::AccessGroup u2_access = ci->AccessFor(uc->user); + + if (matched > 1 && !founder) + continue; + if (u != uc->user && ci->HasFieldS("PEACE") && u2_access >= u_access) + continue; + else if (ci->c->MatchesList(uc->user, "EXCEPT")) + continue; + else if (uc->user->IsProtected()) + continue; + + if (block->Get("kick", "yes")) + { + ++kicked; + + if (ci->HasFieldS("SIGNKICK") || (ci->HasFieldS("SIGNKICK_LEVEL") && !u_access.HasPriv("SIGNKICK"))) + { + reason += " (Matches " + mask + ")"; + signkickformat = signkickformat.replace_all_cs("%m", reason); + c->Kick(ci->WhoSends(), uc->user, "%s", signkickformat.c_str()); + } + else + c->Kick(ci->WhoSends(), uc->user, "%s (Matches %s)", reason.c_str(), mask.c_str()); + } + } + } + + if (matched) + source.Reply(_("Kicked \002{0}/{1}\002 users matching \002{2}\002 from \002{3}\002."), kicked, matched, mask, c->name); + else + source.Reply(_("No users on \002{0}\002 match \002{1}\002."), c->name, mask); + } + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + source.Reply(_("Bans a given \037nick\037 or \037mask\037 on \002channel\002. An optional expiry may be given to cause services to remove the ban after a set amount of time.\n" + "\n" + "Use of this command requires the \002{0}\002 privilege on \037channel\037.\n" + "\n" + "Examples:\n" + " {command} #anope Jobe\n" + " Bans the user \"Jobe\" from \"#anope\".\n" + "\n" + " {command} #anope Guest!*@*\n" + " Bans the mask \"Guest!*@*\" on \"#anope\"."), + "BAN"); + return true; + } +}; + +class CSBan : public Module +{ + CommandCSBan commandcsban; + + public: + CSBan(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR) + , commandcsban(this) + { + me = this; + } +}; + +MODULE_INIT(CSBan) diff --git a/modules/chanserv/clone.cpp b/modules/chanserv/clone.cpp new file mode 100644 index 000000000..256446bd1 --- /dev/null +++ b/modules/chanserv/clone.cpp @@ -0,0 +1,221 @@ +/* ChanServ core functions + * + * (C) 2003-2014 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#include "module.h" +#include "modules/bs_badwords.h" +#include "modules/cs_akick.h" + +class CommandCSClone : public Command +{ +#if 0 + void CopyLevels(CommandSource &source, ChannelInfo *ci, ChannelInfo *target_ci) + { + const Anope::map &cilevels = ci->GetLevelEntries(); + + for (Anope::map::const_iterator it = cilevels.begin(); it != cilevels.end(); ++it) + { + target_ci->SetLevel(it->first, it->second); + } + + source.Reply(_("All level entries from \002%s\002 have been cloned into \002%s\002."), ci->name.c_str(), target_ci->name.c_str()); + } +#endif + +public: + CommandCSClone(Module *creator) : Command(creator, "chanserv/clone", 2, 3) + { + this->SetDesc(_("Copy all settings from one channel to another")); + this->SetSyntax(_("\037channel\037 \037target\037 [\037what\037]")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + const Anope::string &channel = params[0]; + const Anope::string &target = params[1]; + Anope::string what = params.size() > 2 ? params[2] : ""; + + if (Anope::ReadOnly) + { + source.Reply(_("Services are in read-only mode.")); + return; + } + + User *u = source.GetUser(); + ChanServ::Channel *ci = ChanServ::Find(channel); + bool override = false; + + if (ci == NULL) + { + source.Reply(_("Channel \002{0}\002 isn't registered."), channel); + return; + } + + ChanServ::Channel *target_ci = ChanServ::Find(target); + if (!target_ci) + { + source.Reply(_("Channel \002{0}\002 isn't registered."), target); + return; + } + + if (ci == target_ci) + { + source.Reply(_("Cannot clone channel \002{0}\002 to itself!"), ci->GetName()); + return; + } + + if (!source.IsFounder(ci) || !source.IsFounder(target_ci)) + { + if (!source.HasPriv("chanserv/administration")) + { + source.Reply(_("Access denied. You do not have the privilege \002{0}\002 on \002{1}\002 and \002{2}\002."), "FOUNDER", ci->GetName(), target_ci->GetName()); + return; + } + else + override = true; + } + + if (what.equals_ci("ALL")) + what.clear(); + + if (what.empty()) + { + target_ci->Delete(); + target_ci = ChanServ::channel.Create(); + target_ci->SetName(target); + ChanServ::registered_channel_map& map = ChanServ::service->GetChannels(); + map[target_ci->GetName()] = target_ci; + target_ci->c = Channel::Find(target_ci->GetName()); + + if (ci->GetBot()) + ci->GetBot()->Assign(u, target_ci); + else + target_ci->SetBot(nullptr); + + if (target_ci->c) + { + target_ci->c->ci = target_ci; + + target_ci->c->CheckModes(); + + target_ci->c->SetCorrectModes(u, true); + } + + if (target_ci->c && !target_ci->c->topic.empty()) + { + target_ci->SetLastTopic(target_ci->GetLastTopic()); + target_ci->SetLastTopicSetter(target_ci->c->topic_setter); + target_ci->SetLastTopicTime(target_ci->c->topic_time); + } + else + target_ci->SetLastTopicSetter(source.service->nick); + + Event::OnChanRegistered(&Event::ChanRegistered::OnChanRegistered, target_ci); + + source.Reply(_("All settings from \002{0}\002 have been cloned to \002{0}\002."), channel, target); + } + else if (what.equals_ci("ACCESS")) + { + std::set masks; + unsigned access_max = Config->GetModule("chanserv")->Get("accessmax", "1024"); + unsigned count = 0; + + for (unsigned i = 0; i < target_ci->GetAccessCount(); ++i) + masks.insert(target_ci->GetAccess(i)->Mask()); + + for (unsigned i = 0; i < ci->GetAccessCount(); ++i) + { + ChanServ::ChanAccess *taccess = ci->GetAccess(i); + + if (access_max && target_ci->GetDeepAccessCount() >= access_max) + break; + + if (masks.count(taccess->Mask())) + continue; + masks.insert(taccess->Mask()); + + ChanServ::ChanAccess *newaccess = anope_dynamic_static_cast(taccess->GetSerializableType()->Create()); + newaccess->SetChannel(taccess->GetChannel()); + newaccess->SetMask(taccess->GetMask()); + newaccess->SetMask(taccess->GetMask()); + newaccess->SetCreator(taccess->GetCreator()); + newaccess->SetLastSeen(taccess->GetLastSeen()); + newaccess->SetCreated(taccess->GetCreated()); + newaccess->AccessUnserialize(taccess->AccessSerialize()); + + ++count; + } + + source.Reply(_("\002{0}\002 access entries from \002{1}\002 have been cloned to \002{2}\002."), count, channel, target); + } + else if (what.equals_ci("AKICK")) + { + target_ci->ClearAkick(); + for (unsigned i = 0; i < ci->GetAkickCount(); ++i) + { + AutoKick *ak = ci->GetAkick(i); + if (ak->GetAccount()) + target_ci->AddAkick(ak->GetCreator(), ak->GetAccount(), ak->GetReason(), ak->GetAddTime(), ak->GetLastUsed()); + else + target_ci->AddAkick(ak->GetCreator(), ak->GetMask(), ak->GetReason(), ak->GetAddTime(), ak->GetLastUsed()); + } + + source.Reply(_("All auto kick entries from \002{0}\002 have been cloned to \002{1}\002."), channel, target); + } + else if (what.equals_ci("BADWORDS")) + { + if (!badwords) + { + source.Reply(_("All badword entries from \002{0}\002 have been cloned to \002{1}\002."), channel, target); // BotServ doesn't exist/badwords isn't loaded + return; + } + + badwords->ClearBadWords(target_ci); + + for (unsigned i = 0; i < badwords->GetBadWordCount(ci); ++i) + { + BadWord *bw = badwords->GetBadWord(ci, i); + badwords->AddBadWord(target_ci, bw->GetWord(), bw->GetType()); + } + + source.Reply(_("All badword entries from \002{0}\002 have been cloned to \002{1}\002."), channel, target); + } + else + { + this->OnSyntaxError(source, ""); + return; + } + + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to clone " << (what.empty() ? "everything from it" : what) << " to " << target_ci->GetName(); + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + source.Reply(_("Copies all settings, access, akicks, etc. from \037channel\037 to the \037target\037 channel." + " If \037what\037 is \002ACCESS\002, \002AKICK\002, or \002BADWORDS\002 then only the respective settings are cloned.\n" + "\n" + "You must be the founder of \037channel\037 and \037target\037.")); + return true; + } +}; + +class CSClone : public Module +{ + CommandCSClone commandcsclone; + + public: + CSClone(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR) + , commandcsclone(this) + { + + } +}; + +MODULE_INIT(CSClone) diff --git a/modules/chanserv/cs_access.old b/modules/chanserv/cs_access.old new file mode 100644 index 000000000..b363e9757 --- /dev/null +++ b/modules/chanserv/cs_access.old @@ -0,0 +1,941 @@ +/* ChanServ core functions + * + * (C) 2003-2014 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/chanserv.h" +#include "modules/cs_access.h" +#include "main/chanaccess.h" +#include "main/chanaccesstype.h" + +class AccessChanAccess : public ChanAccessImpl +{ + public: + AccessChanAccess(Serialize::TypeBase *type) : ChanAccessImpl(type) { } + AccessChanAccess(Serialize::TypeBase *type, Serialize::ID id) : ChanAccessImpl(type, id) { } + + 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() + { + return stringify(this->GetLevel()); + } + + void AccessUnserialize(const Anope::string &data) override + { + try + { + this->SetLevel(convertTo(data)); + } + catch (const ConvertException &) + { + } + } + +#if 0 + bool operator>(ChanServ::ChanAccess &other) override + { + if (this->GetSerializableType() != other.GetSerializableType()) + return ChanServ::ChanAccess::operator>(other); + else + return this->GetLevel() > anope_dynamic_static_cast(&other)->GetLevel(); + } + + bool operator<(ChanServ::ChanAccess &other) override + { + if (this->GetSerializableType() != other.GetSerializableType()) + return ChanAccess::operator<(other); + else + return this->GetLevel() < anope_dynamic_static_cast(&other)->GetLevel(); + } +#endif +}; + +class AccessChanAccessType : public Serialize::Type +{ + public: + Serialize::Field level; + + AccessChanAccessType(Module *me) : Serialize::Type(me, "AccessChanAccess") + , level(this, "level") + { + SetParent(ChanServ::chanaccess); + } +}; + +int AccessChanAccess::GetLevel() +{ + return Get(&AccessChanAccessType::level); +} + +void AccessChanAccess::SetLevel(const int &i) +{ + Object::Set(&AccessChanAccessType::level, i); +} + +class CommandCSAccess : public Command +{ + void DoAdd(CommandSource &source, ChanServ::Channel *ci, const std::vector ¶ms) + { + Anope::string mask = params[2]; + ChanServ::Privilege *p = NULL; + int level = ChanServ::ACCESS_INVALID; + + try + { + level = convertTo(params[3]); + } + catch (const ConvertException &) + { + p = ChanServ::service ? ChanServ::service->FindPrivilege(params[3]) : 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 tmp_access(nullptr); + tmp_access.SetChannel(ci); + tmp_access.SetLevel(level); + + bool override = false; + + if ((!highest || *highest <= tmp_access) && !u_access.founder) + { + if (source.HasPriv("chanserv/access/modify")) + override = true; + else + { + 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); + return; + } + } + + NickServ::Nick *na = nullptr; + ChanServ::Channel *targ_ci = nullptr; + + if (IRCD->IsChannelValid(mask)) + { + if (Config->GetModule("chanserv")->Get("disallow_channel_access")) + { + source.Reply(_("Channels may not be on access lists.")); + return; + } + + targ_ci = ChanServ::Find(mask); + if (targ_ci == NULL) + { + source.Reply(_("Channel \002{0}\002 isn't registered."), mask); + return; + } + + if (ci == targ_ci) + { + source.Reply(_("You can't add a channel to its own access list.")); + return; + } + + mask = targ_ci->GetName(); + } + else + { + na = NickServ::FindNick(mask); + + if (!na && Config->GetModule("chanserv")->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 *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.HasPriv("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; + } + delete access; + break; + } + } + + unsigned access_max = Config->GetModule("chanserv")->Get("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; + } + + AccessChanAccess *access = anope_dynamic_static_cast(accesschanaccess.Create()); + if (na) + access->SetObj(na->GetAccount()); + else if (targ_ci) + access->SetObj(targ_ci); + access->SetChannel(ci); + access->SetMask(mask); + access->SetCreator(source.GetNick()); + access->SetLevel(level); + access->SetLastSeen(0); + access->SetCreated(Anope::CurTime); + + Event::OnAccessAdd(&Event::AccessAdd::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{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, const std::vector ¶ms) + { + Anope::string mask = params[2]; + + 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) + { + bool override = !source.AccessFor(ci).HasPriv("ACCESS_CHANGE") && source.HasPriv("chanserv/access/modify"); + 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 && !override && access->GetObj() != source.nc) + { + denied = true; + return; + } + + ++deleted; + if (!nicks.empty()) + nicks += ", " + access->Mask(); + else + nicks = access->Mask(); + + Event::OnAccessDel(&Event::AccessDel::OnAccessDel, ci, source, access); + delete access; + }, + [&]() + { + 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 + { + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to delete " << nicks; + + 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->GetObj() != source.nc && !u_access.founder && (!highest || *highest <= *access) && !source.HasPriv("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()); + bool override = !u_access.founder && !u_access.HasPriv("ACCESS_CHANGE") && !access->Mask().equals_ci(source.nc->GetDisplay()); + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "to delete " << access->Mask(); + + Event::OnAccessDel(&Event::AccessDel::OnAccessDel, ci, source, access); + delete access; + } + 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 std::vector ¶ms, ListFormatter &list) + { + const Anope::string &nick = params.size() > 2 ? params[2] : ""; + + 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) + { + ChanServ::ChanAccess::Path p; + if (access->Matches(cit->second->user, cit->second->user->Account(), p)) + 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) + { + ChanServ::ChanAccess::Path p; + if (access->Matches(cit->second->user, cit->second->user->Account(), p)) + 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 std::vector ¶ms) + { + 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, params, list); + } + + void DoView(CommandSource &source, ChanServ::Channel *ci, const std::vector ¶ms) + { + 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, params, list); + } + + void DoClear(CommandSource &source, ChanServ::Channel *ci) + { + if (!source.IsFounder(ci) && !source.HasPriv("chanserv/access/modify")) + { + source.Reply(_("Access denied. You do not have privilege \002{0}\002 on \002{1}\002."), "FOUNDER", ci->GetName()); + return; + } + + Event::OnAccessClear(&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); + Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "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 &s = params.size() > 3 ? params[3] : ""; + + ChanServ::Channel *ci = ChanServ::Find(chan); + if (ci == NULL) + { + 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 (source.HasPriv("chanserv/access/modify")) + has_access = true; + else if (is_list && source.HasPriv("chanserv/access/list")) + has_access = true; + else if (is_list && source.AccessFor(ci).HasPriv("ACCESS_LIST")) + has_access = true; + else if (source.AccessFor(ci).HasPriv("ACCESS_CHANGE")) + has_access = true; + else if (is_del) + { + NickServ::Nick *na = NickServ::FindNick(nick); + if (na && na->GetAccount() == source.GetAccount()) + has_access = true; + } + + /* If LIST, we don't *require* any parameters, but we can take any. + * If DEL, we require a nick and no level. + * Else (ADD), we require a level (which implies a nick). */ + if (is_list || is_clear ? 0 : (cmd.equals_ci("DEL") ? (nick.empty() || !s.empty()) : s.empty())) + { + this->OnSyntaxError(source, cmd); + return; + } + + 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, params); + else if (cmd.equals_ci("DEL")) + this->DoDel(source, ci, params); + else if (cmd.equals_ci("LIST")) + this->DoList(source, ci, params); + else if (cmd.equals_ci("VIEW")) + this->DoView(source, ci, params); + 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.command, ChanServ::ACCESS_INVALID + 1, ChanServ::ACCESS_FOUNDER - 1, "AUTOOP", "ACCESS_CHANGE"); + + if (!Config->GetModule("chanserv")->Get("disallow_channel_access")) + source.Reply(_("The given \037mask\037 may also be a channel, which will use the access list from the other channel up to the given \037level\037.")); + + //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.command, "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 en