/* * * (C) 2003-2025 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/botserv/badwords.h" #include "modules/botserv/kick.h" #include "modules/chanserv/entrymsg.h" #include "modules/chanserv/mode.h" #include "modules/hostserv/request.h" #include "modules/info.h" #include "modules/nickserv/cert.h" #include "modules/operserv/forbid.h" #include "modules/operserv/news.h" #include "modules/operserv/oper.h" #include "modules/operserv/session.h" #include "modules/set_misc.h" #include "modules/suspend.h" // Handles reading from an Atheme database row. class AthemeRow final { private: // The number of failed reads. unsigned error = 0; // The underlying token stream. spacesepstream stream; public: AthemeRow(const Anope::string &str) : stream(str) { } operator bool() const { return !error; } // Retrieves the next parameter. Anope::string Get() { Anope::string token; if (!stream.GetToken(token)) error++; return token; } // Retrieves the next parameter as a number. template std::enable_if_t, Numeric> GetNum() { return Anope::Convert(Get(), 0); } // Retrieves the entire row. Anope::string GetRow() { return stream.GetString(); } // Retrieves the remaining data in the row. Anope::string GetRemaining() { auto remaining = stream.GetRemaining(); if (remaining.empty()) error++; return remaining; } bool LogError(Module *mod) { Log(mod) << "Malformed database line (expected " << error << " fields): " << GetRow(); return false; } }; struct ModeLockData final { char letter; Anope::string name; Anope::string value; bool set; ModeLockData(const Anope::string &n, bool s, const Anope::string &v = "") : letter(0) , name(n) , value(v) , set(s) { } ModeLockData(char l, const Anope::string &v = "") : letter(l) , value(v) , set(true) { } Anope::string str() const { std::stringstream buf; buf << '+' << (name.empty() ? letter : name); if (value.empty()) buf << ' ' << value; return buf.str(); } }; struct ChannelData final { Anope::unordered_map akicks; Anope::string bot; Anope::string info_adder; Anope::string info_message; time_t info_ts = 0; std::vector mlocks; Anope::string suspend_by; Anope::string suspend_reason; time_t suspend_ts = 0; }; struct UserData final { Anope::string info_adder; Anope::string info_message; time_t info_ts = 0; Anope::string last_mask; Anope::string last_quit; Anope::string last_real_mask; bool noexpire = false; bool protect = false; std::optional protectafter; Anope::string suspend_by; Anope::string suspend_reason; time_t suspend_ts = 0; Anope::string vhost; Anope::string vhost_creator; Anope::map vhost_nick; time_t vhost_ts = 0; }; namespace { // Whether we can safely import clones. bool import_clones = true; } class DBAtheme final : public Module { private: ServiceReference accessprov; PrimitiveExtensibleItem chandata; std::map flags; PrimitiveExtensibleItem userdata; ServiceReference sglinemgr; ServiceReference snlinemgr; ServiceReference sqlinemgr; Anope::map> rowhandlers = { { "AC", &DBAtheme::HandleIgnore }, { "AR", &DBAtheme::HandleIgnore }, { "BE", &DBAtheme::HandleBE }, { "BLE", &DBAtheme::HandleIgnore }, { "BOT", &DBAtheme::HandleBOT }, { "BOT-COUNT", &DBAtheme::HandleIgnore }, { "BW", &DBAtheme::HandleBW }, { "CA", &DBAtheme::HandleCA }, { "CF", &DBAtheme::HandleIgnore }, { "CFCHAN", &DBAtheme::HandleIgnore }, { "CFDBV", &DBAtheme::HandleIgnore }, { "CFMD", &DBAtheme::HandleIgnore }, { "CFOP", &DBAtheme::HandleIgnore }, { "CLONES-CD", &DBAtheme::HandleIgnore }, { "CLONES-CK", &DBAtheme::HandleIgnore }, { "CLONES-DBV", &DBAtheme::HandleCLONESDBV }, { "CLONES-EX", &DBAtheme::HandleCLONESEX }, { "CLONES-GR", &DBAtheme::HandleIgnore }, { "CSREQ", &DBAtheme::HandleIgnore }, { "CSREQ", &DBAtheme::HandleIgnore }, { "DBV", &DBAtheme::HandleDBV }, { "GACL", &DBAtheme::HandleIgnore }, { "GDBV", &DBAtheme::HandleIgnore }, { "GE", &DBAtheme::HandleIgnore }, { "GFA", &DBAtheme::HandleIgnore }, { "GRP", &DBAtheme::HandleIgnore }, { "GRVER", &DBAtheme::HandleGRVER }, { "HE", &DBAtheme::HandleIgnore }, { "HO", &DBAtheme::HandleIgnore }, { "HR", &DBAtheme::HandleHR }, { "JM", &DBAtheme::HandleIgnore }, { "KID", &DBAtheme::HandleIgnore }, { "KL", &DBAtheme::HandleKL }, { "LI", &DBAtheme::HandleLI }, { "LIO", &DBAtheme::HandleLIO }, { "LUID", &DBAtheme::HandleIgnore }, { "MC", &DBAtheme::HandleMC }, { "MCFP", &DBAtheme::HandleMCFP }, { "MDA", &DBAtheme::HandleMDA }, { "MDC", &DBAtheme::HandleMDC }, { "MDEP", &DBAtheme::HandleIgnore }, { "MDG", &DBAtheme::HandleIgnore }, { "MDN", &DBAtheme::HandleMDN }, { "MDU", &DBAtheme::HandleMDU }, { "ME", &DBAtheme::HandleME }, { "MI", &DBAtheme::HandleMI }, { "MM", &DBAtheme::HandleMM }, { "MN", &DBAtheme::HandleMN }, { "MU", &DBAtheme::HandleMU }, { "NAM", &DBAtheme::HandleNAM }, { "QID", &DBAtheme::HandleIgnore }, { "QL", &DBAtheme::HandleQL }, { "RM", &DBAtheme::HandleIgnore }, { "RR", &DBAtheme::HandleIgnore }, { "RW", &DBAtheme::HandleIgnore }, { "SI", &DBAtheme::HandleIgnore }, { "SO", &DBAtheme::HandleSO }, { "TS", &DBAtheme::HandleIgnore }, { "XID", &DBAtheme::HandleIgnore }, { "XL", &DBAtheme::HandleXL }, }; void ApplyAccess(Anope::string &in, char flag, Anope::string &out, std::initializer_list privs) { for (const auto *priv : privs) { auto pos = in.find(flag); if (pos != Anope::string::npos) { auto privchar = flags.find(priv); if (privchar != flags.end()) { out.push_back(privchar->second); in.erase(pos, 1); } } } } void ApplyFlags(Extensible *ext, Anope::string &flags, char flag, const char* extname, bool extend = true) { auto pos = flags.find(flag); auto has_flag = (pos != Anope::string::npos); if (has_flag == extend) ext->Extend(extname); else ext->Shrink(extname); if (has_flag) flags.erase(pos, 1); } void ApplyLocks(ChannelInfo *ci, unsigned locks, const Anope::string &limit, const Anope::string &key, bool set) { auto *data = chandata.Require(ci); // Start off with the standard mode values. if (locks & 0x1u) data->mlocks.emplace_back("INVITE", set); if (locks & 0x2u) data->mlocks.emplace_back("KEY", set, key); if (locks & 0x4u) data->mlocks.emplace_back("LIMIT", set, limit); if (locks & 0x8u) data->mlocks.emplace_back("MODERATED", set); if (locks & 0x10u) data->mlocks.emplace_back("NOEXTERNAL", set); if (locks & 0x40u) data->mlocks.emplace_back("PRIVATE", set); if (locks & 0x80u) data->mlocks.emplace_back("SECRET", set); if (locks & 0x100u) data->mlocks.emplace_back("TOPIC", set); if (locks & 0x200u) data->mlocks.emplace_back("REGISTERED", set); // Atheme also supports per-ircd values here (ew). if (IRCD->owner->name == "inspircd") { if (locks & 0x1000u) data->mlocks.emplace_back("BLOCKCOLOR", set); if (locks & 0x2000u) data->mlocks.emplace_back("REGMODERATED", set); if (locks & 0x4000u) data->mlocks.emplace_back("REGISTEREDONLY", set); if (locks & 0x8000u) data->mlocks.emplace_back("OPERONLY", set); if (locks & 0x10000u) data->mlocks.emplace_back("NONOTICE", set); if (locks & 0x20000u) data->mlocks.emplace_back("NOKICK", set); if (locks & 0x40000u) data->mlocks.emplace_back("STRIPCOLOR", set); if (locks & 0x80000u) data->mlocks.emplace_back("NOKNOCK", set); if (locks & 0x100000u) data->mlocks.emplace_back("ALLINVITE", set); if (locks & 0x200000u) data->mlocks.emplace_back("NOCTCP", set); if (locks & 0x400000u) data->mlocks.emplace_back("AUDITORIUM", set); if (locks & 0x800000u) data->mlocks.emplace_back("SSL", set); if (locks & 0x100000u) data->mlocks.emplace_back("NONICK", set); if (locks & 0x200000u) data->mlocks.emplace_back("CENSOR", set); if (locks & 0x400000u) data->mlocks.emplace_back("BLOCKCAPS", set); if (locks & 0x800000u) data->mlocks.emplace_back("PERM", set); if (locks & 0x2000000u) data->mlocks.emplace_back("DELAYEDJOIN", set); } else if (IRCD->owner->name == "ngircd") { if (locks & 0x1000u) data->mlocks.emplace_back("REGISTEREDONLY", set); if (locks & 0x2000u) data->mlocks.emplace_back("OPERONLY", set); if (locks & 0x4000u) data->mlocks.emplace_back("PERM", set); if (locks & 0x8000u) data->mlocks.emplace_back("SSL", set); } else if (IRCD->owner->name == "solanum") { if (locks & 0x1000u) data->mlocks.emplace_back("BLOCKCOLOR", set); if (locks & 0x2000u) data->mlocks.emplace_back("REGISTEREDONLY", set); if (locks & 0x4000u) data->mlocks.emplace_back("OPMODERATED", set); if (locks & 0x8000u) data->mlocks.emplace_back("ALLINVITE", set); if (locks & 0x10000u) data->mlocks.emplace_back("LBAN", set); if (locks & 0x20000u) data->mlocks.emplace_back("PERM", set); if (locks & 0x40000u) data->mlocks.emplace_back("ALLOWFORWARD", set); if (locks & 0x80000u) data->mlocks.emplace_back("NOFORWARD", set); if (locks & 0x100000u) data->mlocks.emplace_back("NOCTCP", set); if (locks & 0x400000u) data->mlocks.emplace_back("SSL", set); if (locks & 0x800000u) data->mlocks.emplace_back("OPERONLY", set); if (locks & 0x1000000u) data->mlocks.emplace_back("ADMINONLY", set); if (locks & 0x2000000u) data->mlocks.emplace_back("NONOTICE", set); if (locks & 0x4000000u) data->mlocks.emplace_back("PROTECTED", set); if (locks & 0x8000000) data->mlocks.emplace_back("NOFILTER", set); if (locks & 0x10000000U) data->mlocks.emplace_back("REGMODERATED", set); } else if (IRCD->owner->name == "unrealircd") { if (locks & 0x1000u) data->mlocks.emplace_back("BLOCKCOLOR", set); if (locks & 0x2000u) data->mlocks.emplace_back("REGMODERATED", set); if (locks & 0x4000u) data->mlocks.emplace_back("REGISTEREDONLY", set); if (locks & 0x8000u) data->mlocks.emplace_back("OPERONLY", set); if (locks & 0x20000u) data->mlocks.emplace_back("NOKICK", set); if (locks & 0x40000u) data->mlocks.emplace_back("STRIPCOLOR", set); if (locks & 0x80000u) data->mlocks.emplace_back("NOKNOCK", set); if (locks & 0x100000u) data->mlocks.emplace_back("NOINVITE", set); if (locks & 0x200000u) data->mlocks.emplace_back("NOCTCP", set); if (locks & 0x800000u) data->mlocks.emplace_back("SSL", set); if (locks & 0x1000000u) data->mlocks.emplace_back("NONICK", set); if (locks & 0x4000000u) data->mlocks.emplace_back("CENSOR", set); if (locks & 0x8000000u) data->mlocks.emplace_back("PERM", set); if (locks & 0x1000000u) data->mlocks.emplace_back("NONOTICE", set); if (locks & 0x2000000u) data->mlocks.emplace_back("DELAYJOIN", set); } else if (IRCD->owner->name != "ratbox") { Log(this) << "Unable to import mode locks for " << IRCD->GetProtocolName(); } } void ApplyPassword(NickCore *nc, Anope::string &flags, const Anope::string &pass) { auto pos = flags.find('C'); if (pos == Anope::string::npos) { // Password is unencrypted so we can use it. Anope::Encrypt(pass, nc->pass); return; } // We are processing an encrypted password. flags.erase(pos, 1); // Atheme supports several password hashing methods. We can only import // some of them currently. // // anope-enc-sha256 Converted to enc_sha256 // argon2 Converted to enc_argon2 // base64 Converted to the first encryption algorithm // bcrypt Converted to enc_bcrypt // crypt3-des NO // crypt3-md5 Converted to enc_posix // crypt3-sha2-256 Converted to enc_posix // crypt3-sha2-512 Converted to enc_posix // ircservices Converted to enc_old // pbkdf2 NO // pbkdf2v2 NO // rawmd5 Converted to enc_md5 // rawsha1 Converted to enc_sha1 // rawsha2-256 Converted to enc_sha2 // rawsha2-512 Converted to enc_sha2 // scrypt NO if (pass.compare(0, 18, "$anope$enc_sha256$", 18) == 0) { auto sep = pass.find('$', 18); Anope::string iv, pass; Anope::B64Decode(pass.substr(18, sep - 18), iv); Anope::B64Decode(pass.substr(sep + 1), pass); nc->pass = "sha256:" + Anope::Hex(pass) + ":" + Anope::Hex(iv); } else if (pass.compare(0, 9, "$argon2d$", 9) == 0) nc->pass = "argon2d:" + pass; else if (pass.compare(0, 9, "$argon2i$", 9) == 0) nc->pass = "argon2i:" + pass; else if (pass.compare(0, 10, "$argon2id$", 10) == 0) nc->pass = "argon2id:" + pass; else if (pass.compare(0, 8, "$base64$", 8) == 0) { Anope::string rawpass; Anope::B64Decode(pass.substr(8), rawpass); Anope::Encrypt(rawpass, nc->pass); } else if (pass.compare(0, 13, "$ircservices$", 13) == 0) nc->pass = "oldmd5:" + pass.substr(13); else if (pass.compare(0, 8, "$rawmd5$", 8) == 0) nc->pass = "md5:" + pass.substr(8); else if (pass.compare(0, 9, "$rawsha1$", 9) == 0) nc->pass = "sha1:" + pass.substr(9); else if (pass.compare(0, 11, "$rawsha256$", 11) == 0) nc->pass = "raw-sha256:" + pass.substr(11); else if (pass.compare(0, 11, "$rawsha512$", 11) == 0) nc->pass = "raw-sha512:" + pass.substr(11); else if (pass.compare(0, 3, "$1$", 3) == 0 || pass.compare(0, 3, "$5", 3) == 0 || pass.compare(0, 3, "$6", 3) == 0) nc->pass = "posix:" + pass; else if (pass.compare(0, 4, "$2a$", 4) == 0 || pass.compare(0, 4, "$2b$", 4) == 0) nc->pass = "bcrypt:" + pass; else { // Generate a new password as we can't use the old one. auto maxpasslen = Config->GetModule("nickserv").Get("maxpasslen", "50"); Anope::Encrypt(Anope::Random(maxpasslen), nc->pass); // If the password is set to * then an external service is being used for authentication. if (pass != "*") Log(this) << "Unable to convert the password for " << nc->display << " as Anope does not support the format!"; } } bool HandleBE(AthemeRow &row) { // BE auto email = row.Get(); auto created = row.GetNum(); auto creator = row.Get(); auto reason = row.GetRemaining(); if (!row) return row.LogError(this); if (!forbid_service) { Log(this) << "Unable to convert forbidden email " << email << " as os_forbid is not loaded"; return true; } auto *forbid = forbid_service->CreateForbid(); forbid->created = created; forbid->creator = creator; forbid->mask = email; forbid->reason = reason; forbid->type = FT_EMAIL; forbid_service->AddForbid(forbid); return true; } bool HandleBOT(AthemeRow &row) { // BOT auto nick = row.Get(); auto user = row.Get(); auto host = row.Get(); auto operonly = row.GetNum(); auto created = row.GetNum(); auto real = row.GetRemaining(); if (!row) return row.LogError(this); auto *bi = BotInfo::Find(nick); if (bi) { Log(this) << "Refusing to import duplicate bot: " << nick; return true; } bi = new BotInfo(nick, user, host, real); bi->oper_only = operonly; bi->created = created; return true; } bool HandleBW(AthemeRow &row) { // BW auto badword = row.Get(); /* auto added = */ row.GetNum(); /* auto creator = */ row.Get(); auto channel = row.Get(); /* auto action = */ row.Get(); if (!row) return row.LogError(this); auto *ci = ChannelInfo::Find(channel); if (!ci) { Log(this) << "Missing ChannelInfo for BW: " << channel; return false; } auto *bw = ci->Require("badwords"); if (!bw) { Log(this) << "Unable to import badwords for " << ci->name << " as bs_kick is not loaded"; return true; } auto *kd = ci->Require("kickerdata"); if (kd) { kd->badwords = true; kd->ttb[TTB_BADWORDS] = 0; } bw->AddBadWord(badword, BW_ANY); return true; } bool HandleCA(AthemeRow &row) { // CA auto channel = row.Get(); auto mask = row.Get(); auto flags = row.Get(); auto modifiedtime = row.GetNum(); auto setter = row.Get(); if (!row) return row.LogError(this); auto *ci = ChannelInfo::Find(channel); if (!ci) { Log(this) << "Missing ChannelInfo for CA: " << channel; return false; } auto *nc = NickCore::Find(mask); if (flags.find('b') != Anope::string::npos) { auto *data = chandata.Require(ci); if (nc) data->akicks[mask] = ci->AddAkick(setter, nc, "", modifiedtime, modifiedtime); else data->akicks[mask] = ci->AddAkick(setter, mask, "", modifiedtime, modifiedtime); return true; } if (!accessprov) { Log(this) << "Unable to import channel access for " << ci->name << " as cs_flags is not loaded"; return true; } Anope::string accessflags; ApplyAccess(flags, 'A', accessflags, { "ACCESS_LIST" }); 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" }); ApplyAccess(flags, 'O', accessflags, { "AUTOOP" }); ApplyAccess(flags, 'o', accessflags, { "OP", "OPME" }); ApplyAccess(flags, 'q', accessflags, { "AUTOOWNER", "OWNER", "OWNERME" }); ApplyAccess(flags, 'r', accessflags, { "KICK" }); ApplyAccess(flags, 's', accessflags, { "SET" }); ApplyAccess(flags, 't', accessflags, { "TOPIC" }); ApplyAccess(flags, 'V', accessflags, { "AUTOVOICE" }); ApplyAccess(flags, 'v', accessflags, { "VOICE", "VOICEME" }); if (!accessflags.empty()) { auto *access = accessprov->Create(); access->SetMask(mask, ci); access->creator = setter; access->description = "Imported from Atheme"; access->last_seen = modifiedtime; access->created = modifiedtime; access->AccessUnserialize(accessflags); ci->AddAccess(access); } if (flags != "+") Log(this) << "Unable to convert channel access flags " << flags << " for " << mask << " on " << ci->name; return true; } bool HandleCLONESDBV(AthemeRow &row) { // CLONES-DBV auto version = row.GetNum(); if (version != 3) { Log(this) << "Clones database is version " << version << " which is not supported!"; import_clones = false; } return true; } bool HandleCLONESEX(AthemeRow &row) { if (!import_clones) return HandleIgnore(row); // CLONES-EX auto ip = row.Get(); auto allowed = row.GetNum(); /* auto warn = */ row.GetNum(); auto expires = row.GetNum(); auto reason = row.GetRemaining(); if (!row) return row.LogError(this); if (!session_service) { Log(this) << "Unable to import session limit for " << ip << " as os_session is not loaded"; return true; } auto *exception = session_service->CreateException(); exception->mask = ip; exception->limit = allowed; exception->who = "Unknown"; exception->time = Anope::CurTime; exception->expires = expires; exception->reason = reason; session_service->AddException(exception); return true; } bool HandleDBV(AthemeRow &row) { // DBV auto version = row.GetNum(); if (version != 12) { Log(this) << "Database is version " << version << " which is not supported!"; return false; } return true; } bool HandleGRVER(AthemeRow &row) { // GRVER auto version = row.GetNum(); if (version != 1) { Log(this) << "Database grammar is version " << version << " which is not supported!"; return false; } return true; } bool HandleHR(AthemeRow &row) { // HR auto nick = row.Get(); auto host = row.Get(); auto reqts = row.GetNum(); /* auto creator = */ row.Get(); if (!row) return row.LogError(this); auto *na = NickAlias::Find(nick); if (!na) { Log(this) << "Missing NickAlias for HR: " << nick; return false; } auto *hr = na->Require("hostrequest"); if (!hr) { Log(this) << "Unable to convert host request for " << na->nick << " as hs_request is not loaded"; return true; } hr->nick = na->nick; hr->ident.clear(); hr->host = host; hr->time = reqts; return true; } bool HandleKL(AthemeRow &row) { // KL auto id = row.Get(); auto user = row.Get(); auto host = row.Get(); auto duration = row.GetNum(); auto settime = row.GetNum(); auto setby = row.Get(); auto reason = row.GetRemaining(); if (!row) return row.LogError(this); if (!sglinemgr) { Log(this) << "Unable to import K-line on " << user << "@" << host << " as operserv is not loaded"; return true; } auto *xl = new XLine(user + "@" + host, setby, settime + duration, reason); xl->id = id; sglinemgr->AddXLine(xl); return true; } bool HandleLI(AthemeRow &row) { // LI auto setter = row.Get(); auto subject = row.Get(); auto ts = row.GetNum(); auto body = row.GetRemaining(); if (!row) return row.LogError(this); if (!news_service) { Log(this) << "Unable to convert logon news as os_news is not loaded"; return true; } auto *ni = news_service->CreateNewsItem(); ni->type = NEWS_LOGON; ni->text = Anope::printf("[%s] %s", subject.c_str(), body.c_str()); ni->who = setter; ni->time = ts; news_service->AddNewsItem(ni); return true; } bool HandleLIO(AthemeRow &row) { // LIO auto setter = row.Get(); auto subject = row.Get(); auto ts = row.GetNum(); auto body = row.GetRemaining(); if (!row) return row.LogError(this); if (!news_service) { Log(this) << "Unable to convert oper news as os_news is not loaded"; return true; } auto *ni = news_service->CreateNewsItem(); ni->type = NEWS_OPER; ni->text = Anope::printf("[%s] %s", subject.c_str(), body.c_str()); ni->who = setter; ni->time = ts; news_service->AddNewsItem(ni); return true; } bool HandleIgnore(AthemeRow &row) { Log(LOG_DEBUG_3) << "Intentionally ignoring Atheme database row: " << row.GetRow(); return true; } bool HandleIgnoreMetadata(const Anope::string &target, const Anope::string &key, const Anope::string &value) { Log(LOG_DEBUG_3) << "Intentionally ignoring Atheme database metadata for " << target << ": " << key << " = " << value; return true; } bool HandleMC(AthemeRow &row) { // MC [] auto channel = row.Get(); auto regtime = row.GetNum(); auto used = row.GetNum(); auto flags = row.Get(); auto mlock_on = row.GetNum(); auto mlock_off = row.GetNum(); auto mlock_limit = row.Get(); if (!row) return row.LogError(this); auto mlock_key = row.Get(); // May not exist. auto *ci = new ChannelInfo(channel); ci->registered = regtime; ci->last_used = used; // No equivalent: elnv ApplyFlags(ci, flags, 'h', "CS_NO_EXPIRE"); ApplyFlags(ci, flags, 'k', "KEEPTOPIC"); ApplyFlags(ci, flags, 'o', "NOAUTOOP"); ApplyFlags(ci, flags, 'p', "CS_PRIVATE"); ApplyFlags(ci, flags, 'r', "RESTRICTED"); ApplyFlags(ci, flags, 't', "TOPICLOCK"); ApplyFlags(ci, flags, 'z', "SECUREOPS"); auto pos = flags.find('a'); if (pos != Anope::string::npos) { ci->SetLevel("ACCESS_CHANGE", 0); flags.erase(pos, 1); } pos = flags.find('f'); if (pos != Anope::string::npos) { auto *kd = ci->Require("kickerdata"); if (kd) { kd->flood = true; kd->floodlines = 10; kd->floodsecs = 60; kd->ttb[TTB_FLOOD] = 0; flags.erase(pos, 1); } else { Log(this) << "Unable to convert the 'f' flag for " << ci->name << " as bs_kick is not loaded"; } } pos = flags.find('g'); if (pos != Anope::string::npos) { auto *bi = Config->GetClient("ChanServ"); if (bi) { bi->Assign(nullptr, ci); flags.erase(pos, 1); } else Log(this) << "Unable to convert the 'g' flag for " << ci->name << " as chanserv is not loaded"; } if (flags != "+") Log(this) << "Unable to convert channel flags " << flags << " for " << ci->name; ApplyLocks(ci, mlock_on, mlock_limit, mlock_key, true); ApplyLocks(ci, mlock_off, mlock_limit, mlock_key, false); return true; } bool HandleMCFP(AthemeRow &row) { // MCFP auto display = row.Get(); auto fingerprint = row.Get(); if (!row) return row.LogError(this); auto *nc = NickCore::Find(display); if (!nc) { Log(this) << "Missing NickCore for MCFP: " << display; return false; } auto *cl = nc->Require("certificates"); if (!cl) { Log(this) << "Unable to convert certificate for " << nc->display << " as ns_cert is not loaded"; return true; } cl->AddCert(fingerprint); return true; } bool HandleMDA(AthemeRow &row) { // MDA auto channel = row.Get(); auto mask = row.Get(); auto key = row.Get(); auto value = row.GetRemaining(); if (!row) return row.LogError(this); auto *ci = ChannelInfo::Find(channel); if (!ci) { Log(this) << "Missing ChannelInfo for MDA: " << channel; return false; } if (key == "reason") { auto *data = chandata.Require(ci); auto akick = data->akicks.find(mask); if (akick != data->akicks.end()) akick->second->reason = value; } else Log(this) << "Unknown channel access metadata for " << mask << " on " << ci->name << ": " << key << " = " << value; return true; } bool HandleMDC(AthemeRow &row) { // MDC auto channel = row.Get(); auto key = row.Get(); auto value = row.GetRemaining(); if (!row) return row.LogError(this); auto *ci = ChannelInfo::Find(channel); if (!ci) { Log(this) << "Missing ChannelInfo for MDC: " << channel; return false; } auto *data = chandata.Require(ci); if (key == "private:botserv:bot-assigned") data->bot = value; else if (key == "private:botserv:bot-handle-fantasy") ci->Extend("BS_FANTASY"); else if (key == "private:botserv:no-bot") ci->Extend("BS_NOBOT"); else if (key == "private:channelts") return HandleIgnoreMetadata(ci->name, key, value); else if (key == "private:close:closer") data->suspend_by = value; else if (key == "private:close:reason") data->suspend_reason = value; else if (key == "private:close:timestamp") data->suspend_ts = Anope::Convert(value, 0); else if (key == "private:entrymsg") { auto *eml = ci->Require("entrymsg"); if (!eml) { Log(this) << "Unable to convert entry message for " << ci->name << " as cs_mode is not loaded"; return true; } auto *msg = eml->Create(); msg->chan = ci->name; msg->creator = "Unknown"; msg->message = value; msg->when = Anope::CurTime; (*eml)->push_back(msg); } else if (key == "private:klinechan:closer") data->suspend_by = value; else if (key == "private:klinechan:reason") data->suspend_reason = value; else if (key == "private:klinechan:timestamp") data->suspend_ts = Anope::Convert(value, 0); else if (key == "private:mark:reason") data->info_message = value; else if (key == "private:mark:setter") data->info_adder = value; else if (key == "private:mark:timestamp") data->info_ts = Anope::Convert(value, 0); else if (key == "private:mlockext") { spacesepstream mlocks(value); for (Anope::string mlock; mlocks.GetToken(mlock); ) data->mlocks.emplace_back(mlock[0], mlock.substr(1)); } else if (key == "private:templates") return HandleIgnoreMetadata(ci->name, key, value); else if (key == "private:topic:setter") ci->last_topic_setter = value; else if (key == "private:topic:text") ci->last_topic = value; else if (key == "private:topic:ts") ci->last_topic_time = Anope::Convert(value, 0); else if (key.compare(0, 14, "private:stats:", 14) == 0) return HandleIgnoreMetadata(ci->name, key, value); else if (key.find(':') == Anope::string::npos) { ExtensibleRef extref("cs_set_misc:" + key.upper()); if (!extref) { Log(this) << "Unknown public channel metadata for " << ci->name << ": " << key << " = " << value; return true; } auto *data = extref->Set(ci); data->object = ci->name; data->name = key; data->data = value; } else Log(this) << "Unknown channel metadata for " << ci->name << ": " << key << " = " << value; return true; } bool HandleMDN(AthemeRow &row) { // MDN auto nick = row.Get(); auto key = row.Get(); auto value = row.GetRemaining(); if (!row) return row.LogError(this); if (!forbid_service) { Log(this) << "Unable to convert forbidden nick " << nick << " metadata as os_forbid is not loaded"; return true; } auto *forbid = forbid_service->FindForbidExact(nick, FT_NICK); if (!forbid) { Log(this) << "Missing forbid for MDN: " << nick; return false; } if (key == "private:mark:reason") forbid->reason = value; else if (key == "private:mark:setter") forbid->creator = value; else if (key == "private:mark:timestamp") forbid->created = Anope::Convert(value, 0); else Log(this) << "Unknown forbidden nick metadata for " << forbid->mask << ": " << key << " = " << value; return true; } bool HandleMDU(AthemeRow &row) { // MDU auto display = row.Get(); auto key = row.Get(); auto value = row.GetRemaining(); if (!row) return row.LogError(this); auto *nc = NickCore::Find(display); if (!nc) { Log(this) << "Missing NickCore for MDU: " << display; return false; } auto *data = userdata.Require(nc); if (key == "private:autojoin") return true; // TODO else if (key == "private:doenforce") data->protect = true; else if (key == "private:enforcetime") data->protectafter = Anope::TryConvert(value); else if (key == "private:freeze:freezer") data->suspend_by = value; else if (key == "private:freeze:reason") data->suspend_reason = value; else if (key == "private:freeze:timestamp") data->suspend_ts = Anope::Convert(value, 0); else if (key == "private:host:actual") data->last_real_mask = value; else if (key == "private:host:vhost") data->last_mask = value; else if (key == "private:lastquit:message") data->last_quit = value; else if (key == "private:loginfail:failnum") return HandleIgnoreMetadata(nc->display, key, value); else if (key == "private:loginfail:lastfailaddr") return HandleIgnoreMetadata(nc->display, key, value); else if (key == "private:loginfail:lastfailtime") return HandleIgnoreMetadata(nc->display, key, value); else if (key == "private:mark:reason") data->info_message = value; else if (key == "private:mark:setter") data->info_adder = value; else if (key == "private:mark:timestamp") data->info_ts = Anope::Convert(value, 0); else if (key == "private:swhois") return HandleIgnoreMetadata(nc->display, key, value); else if (key == "private:usercloak") data->vhost = value; else if (key == "private:usercloak-assigner") data->vhost_creator = value; else if (key == "private:usercloak-timestamp") data->vhost_ts = Anope::Convert(value, 0); else if (key.compare(0, 18, "private:usercloak:", 18) == 0) data->vhost_nick[key.substr(18)] = value; else if (key.find(':') == Anope::string::npos) { ExtensibleRef extref("ns_set_misc:" + key.upper()); if (!extref) { Log(this) << "Unknown public account metadata for " << nc->display << ": " << key << " = " << value; return true; } auto *data = extref->Set(nc); data->object = nc->display; data->name = key; data->data = value; } else Log(this) << "Unknown account metadata for " << nc->display << ": " << key << " = " << value; return true; } bool HandleME(AthemeRow &row) { // ME auto target = row.Get(); auto source = row.Get(); auto sent = row.GetNum(); auto flags = row.GetNum(); auto text = row.GetRemaining(); if (!row) return row.LogError(this); auto *nc = NickCore::Find(target); if (!nc) { Log(this) << "Missing NickCore for ME: " << source; return false; } auto *m = new Memo(); m->mi = &nc->memos; m->owner = nc->display; m->sender = source; m->time = sent; m->text = text; m->unread = flags & 0x1; nc->memos.memos->push_back(m); return true; } bool HandleMI(AthemeRow &row) { // MI auto display = row.Get(); auto ignored = row.Get(); if (!row) return row.LogError(this); auto *nc = NickCore::Find(display); if (!nc) { Log(this) << "Missing NickCore for MI: " << display; return false; } nc->memos.ignores.insert(ignored); return true; } bool HandleMM(AthemeRow &row) { // MM /* auto id = */ row.Get(); /* auto setterid = */ row.Get(); auto setteraccount = row.Get(); /* auto markedid = */ row.Get(); auto markedaccount = row.Get(); auto setts = row.GetNum(); /* auto num = */ row.Get(); auto message = row.GetRemaining(); if (!row) return row.LogError(this); auto *nc = NickCore::Find(markedaccount); if (!nc) { Log(this) << "Missing NickCore for MM: " << markedaccount; return false; } auto *oil = nc->Require("operinfo"); if (oil) { auto *info = oil->Create(); info->target = nc->display; info->info = message; info->adder = setteraccount; info->created = setts; (*oil)->push_back(info); } else { Log(this) << "Unable to convert oper info for " << nc->display << " as os_info is not loaded"; } return true; } bool HandleMN(AthemeRow &row) { // MU auto display = row.Get(); auto nick = row.Get(); auto regtime = row.GetNum(); auto lastseen = row.GetNum(); if (!row) return row.LogError(this); auto *nc = NickCore::Find(display); if (!nc) { Log(this) << "Missing NickCore for MN: " << display; return false; } auto *na = NickAlias::Find(nick); if (na) { Log(this) << "Refusing to import duplicate nick: " << nick; return true; } na = new NickAlias(nick, nc); na->registered = regtime; na->last_seen = lastseen ? regtime : na->registered; auto *data = userdata.Get(nc); if (data) { if (!data->last_mask.empty()) na->last_usermask = data->last_mask; if (!data->last_quit.empty()) na->last_quit = data->last_quit; if (!data->last_real_mask.empty()) na->last_realhost = data->last_real_mask; if (data->noexpire) na->Extend("NS_NO_EXPIRE"); auto vhost = data->vhost; auto nick_vhost = data->vhost_nick.find(nick); if (nick_vhost != data->vhost_nick.end()) vhost = nick_vhost->second; if (!vhost.empty()) na->SetVHost("", vhost, data->vhost_creator, data->vhost_ts); } return true; } bool HandleMU(AthemeRow &row) { // MU /* auto id = */ row.Get(); auto display = row.Get(); auto pass = row.Get(); auto email = row.Get(); auto regtime = row.GetNum(); /* auto lastlogin = */ row.Get(); auto flags = row.Get(); auto language = row.Get(); if (!row) return row.LogError(this); auto *nc = NickCore::Find(display); if (nc) { Log(this) << "Refusing to import duplicate account: " << display; return true; } nc = new NickCore(display); nc->email = email; nc->registered = regtime; ApplyPassword(nc, flags, pass); // No equivalent: bglmNQrS ApplyFlags(nc, flags, 'E', "PROTECT"); ApplyFlags(nc, flags, 'e', "MEMO_MAIL"); ApplyFlags(nc, flags, 'n', "NEVEROP"); ApplyFlags(nc, flags, 'o', "AUTOOP", false); ApplyFlags(nc, flags, 'P', "MSG"); ApplyFlags(nc, flags, 'p', "NS_PRIVATE"); ApplyFlags(nc, flags, 's', "HIDE_EMAIL"); ApplyFlags(nc, flags, 'W', "UNCONFIRMED"); // If an Atheme account was awaiting confirmation but Anope is not // configured to use confirmation then autoconfirm it. const auto &nsregister = Config->GetModule("ns_register").Get("registration"); if (nsregister.equals_ci("none")) nc->Shrink("UNCONFIRMED"); auto pos = flags.find('h'); if (pos != Anope::string::npos) { userdata.Require(nc)->noexpire = true; flags.erase(pos, 1); } if (flags != "+") Log(this) << "Unable to convert account flags " << flags << " for " << nc->display; // No translations yet: bg, cy, da. if (language == "de") nc->language = "de_DE.UTF-8"; else if (language == "en") nc->language = "en_US.UTF-8"; else if (language == "es") nc->language = "es_ES.UTF-8"; else if (language == "fr") nc->language = "fr_FR.UTF-8"; else if (language == "ru") nc->language = "ru_RU.UTF-8"; else if (language == "tr") nc->language = "tr_TR.UTF-8"; else if (language != "default") { Log(this) << "Unable to convert language " << language << " for " << nc->display; } return true; } bool HandleNAM(AthemeRow &row) { // NAM auto nick = row.Get(); if (!row) return row.LogError(this); if (!forbid_service) { Log(this) << "Unable to convert forbidden nick " << nick << " as os_forbid is not loaded"; return true; } auto *forbid = forbid_service->CreateForbid(); forbid->creator = "Unknown"; forbid->mask = nick; forbid->reason = "Unknown"; forbid->type = FT_NICK; forbid_service->AddForbid(forbid); return true; } bool HandleQL(AthemeRow &row) { // QL auto id = row.Get(); auto nick = row.Get(); auto duration = row.GetNum(); auto settime = row.GetNum(); auto setby = row.Get(); auto reason = row.GetRemaining(); if (!row) return row.LogError(this); if (!sglinemgr) { Log(this) << "Unable to import Q-line on " << nick << " as operserv is not loaded"; return true; } auto *xl = new XLine(nick, setby, settime + duration, reason); xl->id = id; sqlinemgr->AddXLine(xl); return true; } bool HandleSO(AthemeRow &row) { // SO auto display = row.Get(); auto type = row.Get(); auto flags = row.Get(); if (!row) return row.LogError(this); auto *nc = NickCore::Find(display); if (!nc) { Log(this) << "Missing NickCore for SO: " << display; return false; } auto *ot = OperType::Find(type); if (!ot) { // Attempt to convert oper types. if (type == "sra") ot = OperType::Find("Services Root"); else if (type == "ircop") ot = OperType::Find("Services Operator"); } if (!ot) { Log(this) << "Unable to convert operator status for " << nc->display << " as there is no equivalent oper type: " << type; return true; } nc->o = new MyOper(nc->display, ot); return true; } bool HandleXL(AthemeRow &row) { // XL auto id = row.Get(); auto real = row.Get(); auto duration = row.GetNum(); auto settime = row.GetNum(); auto setby = row.Get(); auto reason = row.GetRemaining(); if (!row) return row.LogError(this); if (!sglinemgr) { Log(this) << "Unable to import X-line on " << real << " as operserv is not loaded"; return true; } auto *xl = new XLine(real, setby, settime + duration, reason); xl->id = id; snlinemgr->AddXLine(xl); return true; } public: DBAtheme(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, DATABASE | VENDOR) , accessprov("AccessProvider", "access/flags") , chandata(this, "ATHEME_CHANDATA") , userdata(this, "ATHEME_USERDATA") , sglinemgr("XLineManager","xlinemanager/sgline") , snlinemgr("XLineManager","xlinemanager/snline") , sqlinemgr("XLineManager","xlinemanager/sqline") { } void OnReload(Configuration::Conf &conf) override { flags.clear(); for (int i = 0; i < Config->CountBlock("privilege"); ++i) { const auto &priv = Config->GetBlock("privilege", i); const Anope::string &name = priv.Get("name"); const Anope::string &value = priv.Get("flag"); if (!name.empty() && !value.empty()) flags[name] = value[0]; } } EventReturn OnLoadDatabase() override { const auto dbname = Anope::ExpandData(Config->GetModule(this).Get("database", "atheme.db")); std::ifstream fd(dbname.str()); if (!fd.is_open()) { Log(this) << "Unable to open " << dbname << " for reading!"; return EVENT_STOP; } for (Anope::string buf; std::getline(fd, buf.str()); ) { AthemeRow row(buf); auto rowtype = row.Get(); if (!row) continue; // Empty row. auto rowhandler = rowhandlers.find(rowtype); if (rowhandler == rowhandlers.end()) { Log(this) << "Unknown row type: " << row.GetRow(); continue; } if (!rowhandler->second(this, row)) break; } for (const auto &[_, ci] : *RegisteredChannelList) { auto *data = chandata.Get(ci); if (!data) continue; if (!data->bot.empty()) { auto *bi = BotInfo::Find(data->bot); if (bi) bi->Assign(nullptr, ci); } if (!data->info_message.empty()) { auto *oil = ci->Require("operinfo"); if (oil) { auto *info = oil->Create(); info->target = ci->name; info->info = data->info_message; info->adder = data->info_adder.empty() ? "Unknown" : data->info_adder; info->created = data->info_ts; (*oil)->push_back(info); } else { Log(this) << "Unable to convert oper info for " << ci->name << " as os_info is not loaded"; } } if (!data->suspend_reason.empty()) { SuspendInfo si; si.by = data->suspend_by.empty() ? "Unknown" : data->suspend_by; si.expires = 0; si.reason = data->suspend_reason; si.what = ci->name; si.when = data->suspend_ts; ci->Extend("CS_SUSPENDED", si); } } for (const auto &[_, nc] : *NickCoreList) { auto *data = userdata.Get(nc); if (!data) continue; if (!data->info_message.empty()) { auto *oil = nc->Require("operinfo"); if (oil) { auto *info = oil->Create(); info->target = nc->display; info->info = data->info_message; info->adder = data->info_adder.empty() ? "Unknown" : data->info_adder; info->created = data->info_ts; (*oil)->push_back(info); } else { Log(this) << "Unable to convert oper info for " << nc->display << " as os_info is not loaded"; } } if (data->protect) { nc->Extend("PROTECT"); if (data->protectafter) nc->Extend("PROTECT_AFTER", data->protectafter.value()); } if (!data->suspend_reason.empty()) { SuspendInfo si; si.by = data->suspend_by.empty() ? "Unknown" : data->suspend_by; si.expires = 0; si.reason = data->suspend_reason; si.what = nc->display; si.when = data->suspend_ts; nc->Extend("NS_SUSPENDED", si); } } return EVENT_STOP; } void OnUplinkSync(Server *s) override { for (auto &[_, ci] : *RegisteredChannelList) { auto *data = chandata.Get(ci); if (!data) continue; auto *ml = ci->Require("modelocks"); if (!ml) { Log(this) << "Unable to convert mode locks for " << ci->name << " as cs_mode is not loaded"; continue; } for (const auto &mlock : data->mlocks) { ChannelMode *mh; if (mlock.name.empty()) mh = ModeManager::FindChannelModeByChar(mlock.letter); else mh = ModeManager::FindChannelModeByName(mlock.name); if (!mh) { Log(this) << "Unable to find mode while importing mode lock on " << ci->name << ": " << mlock.str(); continue; } ml->SetMLock(mh, mlock.set, mlock.value, "Unknown"); } } Anope::SaveDatabases(); } }; MODULE_INIT(DBAtheme)