/* NickServ core functions * * (C) 2003-2022 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" struct AJoinEntry; struct AJoinList : Serialize::Checker > { AJoinList(Extensible *) : Serialize::Checker >("AJoinEntry") { } ~AJoinList(); }; struct AJoinEntry : Serializable { Serialize::Reference owner; Anope::string channel; Anope::string key; AJoinEntry(Extensible *) : Serializable("AJoinEntry") { } ~AJoinEntry() { AJoinList *channels = owner->GetExt("ajoinlist"); if (channels) { std::vector::iterator it = std::find((*channels)->begin(), (*channels)->end(), this); if (it != (*channels)->end()) (*channels)->erase(it); } } void Serialize(Serialize::Data &sd) const anope_override { if (!this->owner) return; sd["owner"] << this->owner->display; sd["channel"] << this->channel; sd["key"] << this->key; } static Serializable* Unserialize(Serializable *obj, Serialize::Data &sd) { Anope::string sowner; sd["owner"] >> sowner; NickCore *nc = NickCore::Find(sowner); if (nc == NULL) return NULL; AJoinEntry *aj; if (obj) aj = anope_dynamic_static_cast(obj); else { aj = new AJoinEntry(nc); aj->owner = nc; } sd["channel"] >> aj->channel; sd["key"] >> aj->key; if (!obj) { AJoinList *channels = nc->Require("ajoinlist"); (*channels)->push_back(aj); } return aj; } }; AJoinList::~AJoinList() { for (unsigned i = 0; i < (*this)->size(); ++i) delete (*this)->at(i); } class CommandNSAJoin : public Command { void DoList(CommandSource &source, NickCore *nc) { AJoinList *channels = nc->Require("ajoinlist"); if ((*channels)->empty()) source.Reply(_("%s's auto join list is empty."), nc->display.c_str()); else { ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Channel")).AddColumn(_("Key")); for (unsigned i = 0; i < (*channels)->size(); ++i) { AJoinEntry *aj = (*channels)->at(i); ListFormatter::ListEntry entry; entry["Number"] = stringify(i + 1); entry["Channel"] = aj->channel; entry["Key"] = aj->key; list.AddEntry(entry); } source.Reply(_("%s's auto join list:"), nc->display.c_str()); std::vector replies; list.Process(replies); for (unsigned i = 0; i < replies.size(); ++i) source.Reply(replies[i]); } } void DoAdd(CommandSource &source, NickCore *nc, const Anope::string &chans, const Anope::string &keys) { AJoinList *channels = nc->Require("ajoinlist"); Anope::string addedchans; Anope::string alreadyadded; Anope::string invalidkey; commasepstream ksep(keys, true); commasepstream csep(chans); for (Anope::string chan, key; csep.GetToken(chan);) { ksep.GetToken(key); unsigned i = 0; for (; i < (*channels)->size(); ++i) if ((*channels)->at(i)->channel.equals_ci(chan)) break; if ((*channels)->size() >= Config->GetModule(this->owner)->Get("ajoinmax")) { source.Reply(_("Sorry, the maximum of %d auto join entries has been reached."), Config->GetModule(this->owner)->Get("ajoinmax")); return; } else if (i != (*channels)->size()) alreadyadded += chan + ", "; else if (IRCD->IsChannelValid(chan) == false) source.Reply(CHAN_X_INVALID, chan.c_str()); else { Channel *c = Channel::Find(chan); Anope::string k; if (c && c->GetParam("KEY", k) && key != k) { invalidkey += chan + ", "; continue; } AJoinEntry *entry = new AJoinEntry(nc); entry->owner = nc; entry->channel = chan; entry->key = key; (*channels)->push_back(entry); addedchans += chan + ", "; } } if (!alreadyadded.empty()) { alreadyadded = alreadyadded.substr(0, alreadyadded.length() - 2); source.Reply(_("%s is already on %s's auto join list."), alreadyadded.c_str(), nc->display.c_str()); } if (!invalidkey.empty()) { invalidkey = invalidkey.substr(0, invalidkey.length() - 2); source.Reply(_("%s had an invalid key specified, and was thus ignored."), invalidkey.c_str()); } if (addedchans.empty()) return; addedchans = addedchans.substr(0, addedchans.length() - 2); Log(nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to ADD channel " << addedchans << " to " << nc->display; source.Reply(_("%s added to %s's auto join list."), addedchans.c_str(), nc->display.c_str()); } void DoDel(CommandSource &source, NickCore *nc, const Anope::string &chans) { AJoinList *channels = nc->Require("ajoinlist"); Anope::string delchans; Anope::string notfoundchans; commasepstream sep(chans); for (Anope::string chan; sep.GetToken(chan);) { unsigned i = 0; for (; i < (*channels)->size(); ++i) if ((*channels)->at(i)->channel.equals_ci(chan)) break; if (i == (*channels)->size()) notfoundchans += chan + ", "; else { delete (*channels)->at(i); delchans += chan + ", "; } } if (!notfoundchans.empty()) { notfoundchans = notfoundchans.substr(0, notfoundchans.length() - 2); source.Reply(_("%s was not found on %s's auto join list."), notfoundchans.c_str(), nc->display.c_str()); } if (delchans.empty()) return; delchans = delchans.substr(0, delchans.length() - 2); Log(nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to DELETE channel " << delchans << " from " << nc->display; source.Reply(_("%s was removed from %s's auto join list."), delchans.c_str(), nc->display.c_str()); if ((*channels)->empty()) nc->Shrink("ajoinlist"); } public: CommandNSAJoin(Module *creator) : Command(creator, "nickserv/ajoin", 1, 4) { this->SetDesc(_("Manage your auto join list")); this->SetSyntax(_("ADD [\037nickname\037] \037channel\037 [\037key\037]")); this->SetSyntax(_("DEL [\037nickname\037] \037channel\037")); this->SetSyntax(_("LIST [\037nickname\037]")); } void Execute(CommandSource &source, const std::vector ¶ms) anope_override { const Anope::string &cmd = params[0]; Anope::string nick, param, param2; if (cmd.equals_ci("LIST")) nick = params.size() > 1 ? params[1] : ""; else nick = (params.size() > 2 && IRCD->IsChannelValid(params[2])) ? params[1] : ""; NickCore *nc; if (!nick.empty()) { const NickAlias *na = NickAlias::Find(nick); if (na == NULL) { source.Reply(NICK_X_NOT_REGISTERED, nick.c_str()); return; } else if (na->nc != source.GetAccount() && !source.HasCommand("nickserv/ajoin")) { source.Reply(ACCESS_DENIED); return; } nc = na->nc; param = params.size() > 2 ? params[2] : ""; param2 = params.size() > 3 ? params[3] : ""; } else { nc = source.nc; param = params.size() > 1 ? params[1] : ""; param2 = params.size() > 2 ? params[2] : ""; } if (cmd.equals_ci("LIST")) return this->DoList(source, nc); else if (nc->HasExt("NS_SUSPENDED")) source.Reply(NICK_X_SUSPENDED, nc->display.c_str()); else if (param.empty()) this->OnSyntaxError(source, ""); else if (Anope::ReadOnly) source.Reply(READ_ONLY_MODE); else if (cmd.equals_ci("ADD")) return this->DoAdd(source, nc, param, param2); else if (cmd.equals_ci("DEL")) return this->DoDel(source, nc, param); else this->OnSyntaxError(source, ""); } bool OnHelp(CommandSource &source, const Anope::string &subcommand) anope_override { this->SendSyntax(source); source.Reply(" "); source.Reply(_("This command manages your auto join list. When you identify\n" "you will automatically join the channels on your auto join list.\n" "Services Operators may provide a nick to modify other users'\n" "auto join lists.")); return true; } }; class NSAJoin : public Module { CommandNSAJoin commandnsajoin; ExtensibleItem ajoinlist; Serialize::Type ajoinentry_type; public: NSAJoin(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), commandnsajoin(this), ajoinlist(this, "ajoinlist"), ajoinentry_type("AJoinEntry", AJoinEntry::Unserialize) { if (!IRCD || !IRCD->CanSVSJoin) throw ModuleException("Your IRCd does not support SVSJOIN"); } void OnUserLogin(User *u) anope_override { BotInfo *NickServ = Config->GetClient("NickServ"); if (!NickServ) return; AJoinList *channels = u->Account()->GetExt("ajoinlist"); if (channels == NULL) return; /* Set +r now, so we can ajoin users into +R channels */ ModeManager::ProcessModes(); for (unsigned i = 0; i < (*channels)->size(); ++i) { AJoinEntry *entry = (*channels)->at(i); Channel *c = Channel::Find(entry->channel); ChannelInfo *ci; if (c) ci = c->ci; else ci = ChannelInfo::Find(entry->channel); bool need_invite = false; Anope::string key = entry->key; AccessGroup u_access; if (ci != NULL) { if (ci->HasExt("CS_SUSPENDED")) continue; u_access = ci->AccessFor(u); } if (c != NULL) { if (c->FindUser(u) != NULL) continue; else if (c->HasMode("OPERONLY") && !u->HasMode("OPER")) continue; else if (c->HasMode("ADMINONLY") && !u->HasMode("ADMIN")) continue; else if (c->HasMode("SSL") && !(u->HasMode("SSL") || u->HasExt("ssl"))) continue; else if (c->MatchesList(u, "BAN") == true && c->MatchesList(u, "EXCEPT") == false) need_invite = true; else if (c->HasMode("INVITE") && c->MatchesList(u, "INVITEOVERRIDE") == false) need_invite = true; if (c->HasMode("KEY")) { Anope::string k; if (c->GetParam("KEY", k)) { if (u_access.HasPriv("GETKEY")) key = k; else if (key != k) need_invite = true; } } if (c->HasMode("LIMIT")) { Anope::string l; if (c->GetParam("LIMIT", l)) { try { unsigned limit = convertTo(l); if (c->users.size() >= limit) need_invite = true; } catch (const ConvertException &) { } } } } if (need_invite && c != NULL) { if (!u_access.HasPriv("INVITE")) continue; IRCD->SendInvite(NickServ, c, u); } IRCD->SendSVSJoin(NickServ, u, entry->channel, key); } } }; MODULE_INIT(NSAJoin)