From 67026b184ddbae859a8f778dc8d7b964645f13c6 Mon Sep 17 00:00:00 2001 From: Kufat Date: Sun, 28 Jun 2026 20:28:59 -0400 Subject: [PATCH] Enhance Atheme database import functionality. --- include/modules/nickserv/ajoin.h | 47 ++++++++ modules/database/db_atheme.cpp | 190 ++++++++++++++++++++++++++++--- modules/nickserv/ns_ajoin.cpp | 39 ++----- 3 files changed, 226 insertions(+), 50 deletions(-) create mode 100644 include/modules/nickserv/ajoin.h diff --git a/include/modules/nickserv/ajoin.h b/include/modules/nickserv/ajoin.h new file mode 100644 index 000000000..7292f3735 --- /dev/null +++ b/include/modules/nickserv/ajoin.h @@ -0,0 +1,47 @@ +// Anope IRC Services +// +// Copyright (C) 2003-2026 Anope Contributors +// +// Anope is free software. You can use, modify, and/or distribute it under the +// terms of version 2 of the GNU General Public License. See docs/LICENSE.txt +// for the complete terms of this license and docs/AUTHORS.txt for a list of +// contributors. +// +// Based on the original code of Epona by Lara +// Based on the original code of Services by Andy Church +// +// SPDX-License-Identifier: GPL-2.0-only + +#pragma once + +#define NICKSERV_AJOIN_LIST_EXT "ajoinlist" + +struct AJoinEntry; + +struct AJoinList + : Serialize::Checker > +{ + AJoinList(Extensible *) : Serialize::Checker >("AJoinEntry") { } + virtual ~AJoinList() = default; +}; + +struct AJoinEntry final + : Serializable +{ + Serialize::Reference owner; + Anope::string channel; + Anope::string key; + + AJoinEntry(Extensible *) : Serializable("AJoinEntry") { } + + ~AJoinEntry() override + { + auto *channels = owner->GetExt(NICKSERV_AJOIN_LIST_EXT); + if (channels) + { + auto it = std::find((*channels)->begin(), (*channels)->end(), this); + if (it != (*channels)->end()) + (*channels)->erase(it); + } + } +}; diff --git a/modules/database/db_atheme.cpp b/modules/database/db_atheme.cpp index 881bbf127..8e385148d 100644 --- a/modules/database/db_atheme.cpp +++ b/modules/database/db_atheme.cpp @@ -12,6 +12,7 @@ // // SPDX-License-Identifier: GPL-2.0-only +#include #include "module.h" #include "modules/botserv/badwords.h" @@ -21,6 +22,7 @@ #include "modules/chanserv/mode.h" #include "modules/hostserv/request.h" #include "modules/info.h" +#include "modules/nickserv/ajoin.h" #include "modules/nickserv/cert.h" #include "modules/operserv/forbid.h" #include "modules/operserv/news.h" @@ -73,10 +75,10 @@ public: } // Retrieves the remaining data in the row. - Anope::string GetRemaining() + Anope::string GetRemaining(bool allow_empty = false) { auto remaining = stream.GetRemaining(); - if (remaining.empty()) + if (remaining.empty() && !allow_empty) error++; return remaining; } @@ -120,14 +122,49 @@ struct ModeLockData final } }; +struct FounderSuccessorCandidate +{ + // Flags ranked from founder down, for successor candidate ranking + static const Anope::string FLAG_PRIORITY; + + NickCore* nc; + size_t priority; + time_t mtime; + + FounderSuccessorCandidate() + : nc(nullptr) + , priority(99) // arbitrary; for log readability + , mtime(std::numeric_limits::max()) + { + } + + FounderSuccessorCandidate(NickCore *c, const Anope::string &flags, time_t m) + : nc(c) + , priority(FLAG_PRIORITY.find_first_of(flags)) + , mtime(m) + { + } + + bool operator<(const FounderSuccessorCandidate& other) + { + return std::tie(priority, mtime) < std::tie(other.priority, other.mtime); + } + + FounderSuccessorCandidate& operator=(const FounderSuccessorCandidate& other) = default; +}; + +const Anope::string FounderSuccessorCandidate::FLAG_PRIORITY = "FSRsaOoHh"; + struct ChannelData final { Anope::unordered_map akicks; Anope::string bot; + FounderSuccessorCandidate founder_candidate; Anope::string info_adder; Anope::string info_message; time_t info_ts = 0; std::vector mlocks; + FounderSuccessorCandidate successor_candidate; Anope::string suspend_by; Anope::string suspend_reason; time_t suspend_ts = 0; @@ -135,6 +172,7 @@ struct ChannelData final struct UserData final { + Anope::map ajoins; Anope::string info_adder; Anope::string info_message; time_t info_ts = 0; @@ -236,21 +274,40 @@ private: { "XL", &DBAtheme::HandleXL }, }; - void ApplyAccess(Anope::string &in, char flag, Anope::string &out, std::initializer_list privs) + static void RemoveAll(Anope::string& in, const Anope::string& unwanted) { - for (const auto *priv : privs) + auto it = std::remove_if(in.begin(), in.end(), [&](char c){ + return unwanted.find_first_of(c) != unwanted.npos; + }); + in.erase(it, in.end()); + } + + static bool RemoveFirstOccurrence(Anope::string& in, char c) + { + auto pos = in.find(c); + if (pos != Anope::string::npos) { - auto pos = in.find(flag); - if (pos != Anope::string::npos) + in.erase(pos, 1); + return true; + } + return false; + } + + bool ApplyAccess(Anope::string &in, char flag, Anope::string &out, std::initializer_list privs) + { + const bool flag_found = RemoveFirstOccurrence(in, flag); + if (flag_found) + { + for (const auto *priv : privs) { auto privchar = flags.find(priv); if (privchar != flags.end()) { out.push_back(privchar->second); - in.erase(pos, 1); } } } + return flag_found; } void ApplyFlags(Extensible *ext, Anope::string &flags, char flag, const char *extname, bool extend = true) @@ -609,16 +666,17 @@ private: return false; } + auto *data = chandata.Require(ci); + auto *nc = NickCore::Find(mask); if (flags.find('b') != Anope::string::npos) { - if (ChanServ::akick_service) + if (!ChanServ::akick_service) { Log(this) << "Unable to import channel akick for " << ci->name << " as cs_akick is not loaded"; return true; } - auto *data = chandata.Require(ci); if (nc) data->akicks[mask] = ChanServ::akick_service->AddAKick(ci, setter, nc, "", modifiedtime, modifiedtime); else @@ -638,7 +696,6 @@ private: ApplyAccess(flags, 'a', accessflags, { "AUTOPROTECT", "PROTECT", "PROTECTME" }); ApplyAccess(flags, 'e', accessflags, { "GETKEY", "NOKICK", "UNBANME" }); ApplyAccess(flags, 'f', accessflags, { "ACCESS_CHANGE" }); - ApplyAccess(flags, 'F', accessflags, { "FOUNDER" }); ApplyAccess(flags, 'H', accessflags, { "AUTOHALFOP" }); ApplyAccess(flags, 'h', accessflags, { "HALFOP", "HALFOPME" }); ApplyAccess(flags, 'i', accessflags, { "INVITE" }); @@ -662,6 +719,35 @@ private: ci->AddAccess(access); } + // Atheme allows multiple founders and picks a successor based on rank if one is not explicitly assigned. + bool is_founder_candidate = RemoveFirstOccurrence(flags, 'F'); + RemoveAll(flags, "SR"); + + FounderSuccessorCandidate current_candidate(nc, originalflags, modifiedtime); + + if (nc && current_candidate < data->successor_candidate) + { + if (is_founder_candidate && current_candidate < data->founder_candidate) + { + Log(LOG_DEBUG) << ci->name << ": Demoting founder candidate (" + << ( data->founder_candidate.nc ? data->founder_candidate.nc->display : "NONE" ) + << ", " << data->founder_candidate.priority + << ") to successor; replacing with (" << current_candidate.nc->display + << ", " << current_candidate.priority << ")"; + data->successor_candidate = data->founder_candidate; + data->founder_candidate = current_candidate; + } + else + { + Log(LOG_DEBUG) << ci->name << ": Replacing successor candidate (" + << ( data->successor_candidate.nc ? data->successor_candidate.nc->display : "NONE" ) + << ", " << data->successor_candidate.priority + << ") with (" << current_candidate.nc->display + << ", " << current_candidate.priority << ")"; + data->successor_candidate = current_candidate; + } + } + if (flags != "+") Log(this) << "Unable to convert channel access flags " << flags << " for " << mask << " on " << ci->name; @@ -788,7 +874,7 @@ private: return true; } - auto *xl = new XLine(user + "@" + host, setby, settime + duration, reason); + auto *xl = new XLine(user + "@" + host, setby, duration ? settime + duration : 0, reason); xl->id = id; sglinemgr->AddXLine(xl); return true; @@ -879,6 +965,7 @@ private: ci->last_used = used; // No equivalent: elnv + RemoveFirstOccurrence(flags, 'v'); // verbose, com ApplyFlags(ci, flags, 'h', "CS_NO_EXPIRE"); ApplyFlags(ci, flags, 'k', "KEEPTOPIC"); ApplyFlags(ci, flags, 'o', "NOAUTOOP"); @@ -985,6 +1072,10 @@ private: if (akick != data->akicks.end()) akick->second->reason = value; } + else if (key == "expires") + { + Log(this) << "Unable to set access expiration for " << mask << " on " << ci->name << ": unimplemented"; + } else Log(this) << "Unknown channel access metadata for " << mask << " on " << ci->name << ": " << key << " = " << value; @@ -1146,10 +1237,7 @@ private: // MDU auto display = row.Get(); auto key = row.Get(); - auto value = row.GetRemaining(); - - if (!row) - return row.LogError(this); + auto value = row.GetRemaining(true); auto *nc = NickCore::Find(display); if (!nc) @@ -1160,7 +1248,17 @@ private: auto *data = userdata.Require(nc); if (key == "private:autojoin") - return true; // TODO + { + commasepstream autojoins(value, true); + for (Anope::string autojoin; autojoins.GetToken(autojoin); ) + { + spacesepstream entry(autojoin); + Anope::string cname, ckey; + if (entry.GetToken(cname)) + entry.GetToken(ckey); + data->ajoins[cname] = ckey; + } + } else if (key == "private:doenforce") data->protect = true; else if (key == "private:enforcetime") @@ -1189,6 +1287,12 @@ private: data->info_adder = value; else if (key == "private:mark:timestamp") data->info_ts = Anope::Convert(value, 0); + else if (key == "private:sendpass:sender") + return HandleIgnoreMetadata(nc->display, key, value); + else if (key == "private:sendpass:timestamp") + return HandleIgnoreMetadata(nc->display, key, value); + else if (key == "private:setpass:key") + return HandleIgnoreMetadata(nc->display, key, value); else if (key == "private:swhois") return HandleIgnoreMetadata(nc->display, key, value); else if (key == "private:usercloak") @@ -1410,6 +1514,7 @@ private: ApplyPassword(nc, flags, pass); // No equivalent: bglmNQrS + RemoveFirstOccurrence(flags, 'b'); // nick b flag is ephemeral, ignore ApplyFlags(nc, flags, 'E', "PROTECT"); ApplyFlags(nc, flags, 'e', "MEMO_MAIL"); ApplyFlags(nc, flags, 'n', "NEVEROP"); @@ -1499,7 +1604,7 @@ private: return true; } - auto *xl = new XLine(nick, setby, settime + duration, reason); + auto *xl = new XLine(nick, setby, duration ? settime + duration : 0, reason); xl->id = id; sqlinemgr->AddXLine(xl); return true; @@ -1561,7 +1666,7 @@ private: return true; } - auto *xl = new XLine(real, setby, settime + duration, reason); + auto *xl = new XLine(real, setby, duration ? settime + duration : 0, reason); xl->id = id; snlinemgr->AddXLine(xl); return true; @@ -1689,6 +1794,55 @@ public: if (!data) continue; + if (!data->ajoins.empty()) + { + auto *channels = nc->Require(NICKSERV_AJOIN_LIST_EXT); + if (channels) + { + for (const auto& ajoin : data->ajoins) + { + auto &channel = ajoin.first, &key = ajoin.second; + if (!key.empty()) + { + Channel *c = Channel::Find(channel); + Anope::string k; + if (c && c->GetParam("KEY", k) && key != k) + { + Log(this) << "Skipping ajoin with incorrect key for channel " << channel + << ", user " << nc->display; + continue; + } + } + + if (!IRCD->IsChannelValid(channel)) + { + Log(this) << "Invalid ajoin channel " << channel << " for " << nc->display; + } + else + { + const auto it = std::find_if((*channels)->cbegin(), (*channels)->cend(), [&](const AJoinEntry* a){ + return a->channel == channel; + }); + + if (it != (*channels)->cend()) + { + Log(this) << "Skipping duplicate ajoin channel" << channel << " for " << nc->display; + continue; + } + auto* entry = new AJoinEntry(nc); + entry->owner = nc; + entry->channel = channel; + entry->key = key; + (*channels)->push_back(entry); // ignore ajoinmax for non-disruptive migration + } + } + } + else + { + Log(this) << "Unable to convert autojoins for " << nc->display << " as ns_ajoin is not loaded"; + } + } + if (!data->info_message.empty()) { auto *oil = nc->Require("operinfo"); diff --git a/modules/nickserv/ns_ajoin.cpp b/modules/nickserv/ns_ajoin.cpp index 5c31ac978..9b100e0cb 100644 --- a/modules/nickserv/ns_ajoin.cpp +++ b/modules/nickserv/ns_ajoin.cpp @@ -13,34 +13,15 @@ // SPDX-License-Identifier: GPL-2.0-only #include "module.h" +#include "modules/nickserv/ajoin.h" -struct AJoinEntry; - -struct AJoinList final - : Serialize::Checker > +struct AJoinListImpl final : public AJoinList { - AJoinList(Extensible *) : Serialize::Checker >("AJoinEntry") { } - ~AJoinList(); -}; - -struct AJoinEntry final - : Serializable -{ - Serialize::Reference owner; - Anope::string channel; - Anope::string key; - - AJoinEntry(Extensible *) : Serializable("AJoinEntry") { } - - ~AJoinEntry() override + AJoinListImpl(Extensible * e) : AJoinList(e) { } + ~AJoinListImpl() { - auto *channels = owner->GetExt("ajoinlist"); - if (channels) - { - auto it = std::find((*channels)->begin(), (*channels)->end(), this); - if (it != (*channels)->end()) - (*channels)->erase(it); - } + for (const auto *ajoin : *(*this)) + delete ajoin; } }; @@ -93,12 +74,6 @@ struct AJoinEntryType final } }; -AJoinList::~AJoinList() -{ - for (const auto *ajoin : *(*this)) - delete ajoin; -} - class CommandNSAJoin final : public Command { @@ -321,7 +296,7 @@ class NSAJoin final : public Module { CommandNSAJoin commandnsajoin; - ExtensibleItem ajoinlist; + ExtensibleItem ajoinlist; AJoinEntryType ajoinentry_type; public: