/*
 * Copyright (C) 2004-2025 ZNC, see the NOTICE file for details.
 * Copyright (C) 2008 by Stefan Rado
 * based on admin.cpp by Sebastian Ramacher
 * based on admin.cpp in crox branch
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <znc/User.h>
#include <znc/IRCNetwork.h>
#include <znc/Chan.h>
#include <znc/IRCSock.h>
#include <znc/Server.h>

using std::map;
using std::vector;

template <std::size_t N>
struct array_size_helper {
    char __place_holder[N];
};

template <class T, std::size_t N>
static array_size_helper<N> array_size(T (&)[N]) {
    return array_size_helper<N>();
}

#define ARRAY_SIZE(array) sizeof(array_size((array)))

class CAdminMod : public CModule {
    using CModule::PutModule;

    struct Setting {
        const char* name;
        CString type;
    };

    void PrintVarsHelp(const CString& sFilter, const Setting vars[],
                       unsigned int uSize, const CString& sDescription) {
        CTable VarTable;
        VarTable.AddColumn(t_s("Type", "helptable"));
        VarTable.AddColumn(t_s("Variables", "helptable"));
        VarTable.SetStyle(CTable::ListStyle);
        std::map<CString, VCString> mvsTypedVariables;
        for (unsigned int i = 0; i != uSize; ++i) {
            CString sVar = CString(vars[i].name).AsLower();
            if (sFilter.empty() || sVar.StartsWith(sFilter) ||
                sVar.WildCmp(sFilter)) {
                mvsTypedVariables[vars[i].type].emplace_back(vars[i].name);
            }
        }
        for (const auto& i : mvsTypedVariables) {
            VarTable.AddRow();
            VarTable.SetCell(t_s("Type", "helptable"), i.first);
            VarTable.SetCell(
                t_s("Variables", "helptable"),
                CString(", ").Join(i.second.cbegin(), i.second.cend()));
        }
        if (!VarTable.empty()) {
            PutModule(sDescription);
            PutModule(VarTable);
        }
    }

    void PrintHelp(const CString& sLine) {
        HandleHelpCommand(sLine);

        const CString str = t_s("String");
        const CString boolean = t_s("Boolean (true/false)");
        const CString integer = t_s("Integer");
        const CString number = t_s("Number");

        const CString sCmdFilter = sLine.Token(1, false);
        const CString sVarFilter = sLine.Token(2, true).AsLower();

        if (sCmdFilter.empty() || sCmdFilter.StartsWith("Set") ||
            sCmdFilter.StartsWith("Get")) {
            Setting vars[] = {
                {"Nick", str},
                {"Altnick", str},
                {"Ident", str},
                {"RealName", str},
                {"BindHost", str},
                {"MultiClients", boolean},
                {"DenyLoadMod", boolean},
                {"DenySetBindHost", boolean},
                {"DenySetIdent", boolean},
                {"DenySetNetwork", boolean},
                {"DenySetRealName", boolean},
                {"DenySetQuitMsg", boolean},
                {"DenySetCTCPReplies", boolean},
                {"DefaultChanModes", str},
                {"QuitMsg", str},
                {"ChanBufferSize", integer},
                {"QueryBufferSize", integer},
                {"AutoClearChanBuffer", boolean},
                {"AutoClearQueryBuffer", boolean},
                {"Password", str},
                {"JoinTries", integer},
                {"MaxJoins", integer},
                {"MaxNetworks", integer},
                {"MaxQueryBuffers", integer},
                {"Timezone", str},
                {"Admin", boolean},
                {"AppendTimestamp", boolean},
                {"PrependTimestamp", boolean},
                {"AuthOnlyViaModule", boolean},
                {"TimestampFormat", str},
                {"DCCBindHost", str},
                {"StatusPrefix", str},
                {"NoTrafficTimeout", integer},
#ifdef HAVE_I18N
                {"Language", str},
#endif
#ifdef HAVE_ICU
                {"ClientEncoding", str},
#endif
            };
            PutModule("");
            PrintVarsHelp(sVarFilter, vars, ARRAY_SIZE(vars),
                          t_s("The following variables are available when "
                              "using the Set/Get commands:"));
        }

        if (sCmdFilter.empty() || sCmdFilter.StartsWith("SetNetwork") ||
            sCmdFilter.StartsWith("GetNetwork")) {
            Setting nvars[] = {
                {"Nick", str},
                {"Altnick", str},
                {"Ident", str},
                {"RealName", str},
                {"BindHost", str},
                {"FloodRate", number},
                {"FloodBurst", integer},
                {"JoinDelay", integer},
#ifdef HAVE_ICU
                {"Encoding", str},
#endif
                {"QuitMsg", str},
                {"TrustAllCerts", boolean},
                {"TrustPKI", boolean},
            };
            PutModule("");
            PrintVarsHelp(sVarFilter, nvars, ARRAY_SIZE(nvars),
                          t_s("The following variables are available when "
                              "using the SetNetwork/GetNetwork commands:"));
        }

        if (sCmdFilter.empty() || sCmdFilter.StartsWith("SetChan") ||
            sCmdFilter.StartsWith("GetChan")) {
            Setting cvars[] = {{"DefModes", str},
                               {"Key", str},
                               {"BufferSize", integer},
                               {"InConfig", boolean},
                               {"AutoClearChanBuffer", boolean},
                               {"Detached", boolean}};
            PutModule("");
            PrintVarsHelp(sVarFilter, cvars, ARRAY_SIZE(cvars),
                          t_s("The following variables are available when "
                              "using the SetChan/GetChan commands:"));
        }

        if (sCmdFilter.empty()) {
            PutModule("");
            PutModule(
                t_s("You can use $user as the user name and $network as the "
                    "network name for modifying your own user and network."));
        }
    }

    CUser* FindUser(const CString& sUsername) {
        if (sUsername.Equals("$me") || sUsername.Equals("$user"))
            return GetUser();
        CUser* pUser = CZNC::Get().FindUser(sUsername);
        if (!pUser) {
            PutModule(t_f("Error: User [{1}] does not exist!")(sUsername));
            return nullptr;
        }
        if (pUser != GetUser() && !GetUser()->IsAdmin()) {
            PutModule(t_s(
                "Error: You need to have admin rights to modify other users!"));
            return nullptr;
        }
        return pUser;
    }

    CIRCNetwork* FindNetwork(CUser* pUser, const CString& sNetwork) {
        if (sNetwork.Equals("$net") || sNetwork.Equals("$network")) {
            if (pUser != GetUser()) {
                PutModule(t_s(
                    "Error: You cannot use $network to modify other users!"));
                return nullptr;
            }
            return CModule::GetNetwork();
        }
        CIRCNetwork* pNetwork = pUser->FindNetwork(sNetwork);
        if (!pNetwork) {
            PutModule(
                t_f("Error: User {1} does not have a network named [{2}].")(
                    pUser->GetUsername(), sNetwork));
        }
        return pNetwork;
    }

    void Get(const CString& sLine) {
        const CString sVar = sLine.Token(1).AsLower();
        CString sUsername = sLine.Token(2, true);
        CUser* pUser;

        if (sVar.empty()) {
            PutModule(t_s("Usage: Get <variable> [username]"));
            return;
        }

        if (sUsername.empty()) {
            pUser = GetUser();
        } else {
            pUser = FindUser(sUsername);
        }

        if (!pUser) return;

        if (sVar == "nick")
            PutModule("Nick = " + pUser->GetNick());
        else if (sVar == "altnick")
            PutModule("AltNick = " + pUser->GetAltNick());
        else if (sVar == "ident")
            PutModule("Ident = " + pUser->GetIdent());
        else if (sVar == "realname")
            PutModule("RealName = " + pUser->GetRealName());
        else if (sVar == "bindhost")
            PutModule("BindHost = " + pUser->GetBindHost());
        else if (sVar == "multiclients")
            PutModule("MultiClients = " + CString(pUser->MultiClients()));
        else if (sVar == "denyloadmod")
            PutModule("DenyLoadMod = " + CString(pUser->DenyLoadMod()));
        else if (sVar == "denysetbindhost")
            PutModule("DenySetBindHost = " + CString(pUser->DenySetBindHost()));
        else if (sVar == "denysetident")
            PutModule("DenySetIdent = " + CString(pUser->DenySetIdent()));
        else if (sVar == "denysetnetwork")
            PutModule("DenySetNetwork = " + CString(pUser->DenySetNetwork()));
        else if (sVar == "denysetrealname")
            PutModule("DenySetRealName = " + CString(pUser->DenySetRealName()));
        else if (sVar == "denysetquitmsg")
            PutModule("DenySetQuitMsg = " + CString(pUser->DenySetQuitMsg()));
        else if (sVar == "denysetctcpreplies")
            PutModule("DenySetCTCPReplies = " + CString(pUser->DenySetCTCPReplies()));
        else if (sVar == "defaultchanmodes")
            PutModule("DefaultChanModes = " + pUser->GetDefaultChanModes());
        else if (sVar == "quitmsg")
            PutModule("QuitMsg = " + pUser->GetQuitMsg());
        else if (sVar == "buffercount")
            PutModule("BufferCount = " + CString(pUser->GetBufferCount()));
        else if (sVar == "chanbuffersize")
            PutModule("ChanBufferSize = " +
                      CString(pUser->GetChanBufferSize()));
        else if (sVar == "querybuffersize")
            PutModule("QueryBufferSize = " +
                      CString(pUser->GetQueryBufferSize()));
        else if (sVar == "keepbuffer")
            // XXX compatibility crap, added in 0.207
            PutModule("KeepBuffer = " + CString(!pUser->AutoClearChanBuffer()));
        else if (sVar == "autoclearchanbuffer")
            PutModule("AutoClearChanBuffer = " +
                      CString(pUser->AutoClearChanBuffer()));
        else if (sVar == "autoclearquerybuffer")
            PutModule("AutoClearQueryBuffer = " +
                      CString(pUser->AutoClearQueryBuffer()));
        else if (sVar == "maxjoins")
            PutModule("MaxJoins = " + CString(pUser->MaxJoins()));
        else if (sVar == "notraffictimeout")
            PutModule("NoTrafficTimeout = " +
                      CString(pUser->GetNoTrafficTimeout()));
        else if (sVar == "maxnetworks")
            PutModule("MaxNetworks = " + CString(pUser->MaxNetworks()));
        else if (sVar == "maxquerybuffers")
            PutModule("MaxQueryBuffers = " + CString(pUser->MaxQueryBuffers()));
        else if (sVar == "jointries")
            PutModule("JoinTries = " + CString(pUser->JoinTries()));
        else if (sVar == "timezone")
            PutModule("Timezone = " + pUser->GetTimezone());
        else if (sVar == "appendtimestamp")
            PutModule("AppendTimestamp = " +
                      CString(pUser->GetTimestampAppend()));
        else if (sVar == "prependtimestamp")
            PutModule("PrependTimestamp = " +
                      CString(pUser->GetTimestampPrepend()));
        else if (sVar == "authonlyviamodule")
            PutModule("AuthOnlyViaModule = " +
                      CString(pUser->AuthOnlyViaModule()));
        else if (sVar == "timestampformat")
            PutModule("TimestampFormat = " + pUser->GetTimestampFormat());
        else if (sVar == "dccbindhost")
            PutModule("DCCBindHost = " + CString(pUser->GetDCCBindHost()));
        else if (sVar == "admin")
            PutModule("Admin = " + CString(pUser->IsAdmin()));
        else if (sVar == "statusprefix")
            PutModule("StatusPrefix = " + pUser->GetStatusPrefix());
#ifdef HAVE_I18N
        else if (sVar == "language")
            PutModule("Language = " + (pUser->GetLanguage().empty()
                                           ? "en"
                                           : pUser->GetLanguage()));
#endif
#ifdef HAVE_ICU
        else if (sVar == "clientencoding")
            PutModule("ClientEncoding = " + pUser->GetClientEncoding());
#endif
        else
            PutModule(t_s("Error: Unknown variable"));
    }

    void Set(const CString& sLine) {
        const CString sVar = sLine.Token(1).AsLower();
        CString sUsername = sLine.Token(2);
        CString sValue = sLine.Token(3, true);

        if (sValue.empty()) {
            PutModule(t_s("Usage: Set <variable> <username> <value>"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        if (sVar == "nick") {
            pUser->SetNick(sValue);
            PutModule("Nick = " + sValue);
        } else if (sVar == "altnick") {
            pUser->SetAltNick(sValue);
            PutModule("AltNick = " + sValue);
        } else if (sVar == "ident") {
            if (!pUser->DenySetIdent() || GetUser()->IsAdmin()) {
                pUser->SetIdent(sValue);
                PutModule("Ident = " + sValue);
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "realname") {
            if (!pUser->DenySetRealName() || GetUser()->IsAdmin()) {
                pUser->SetRealName(sValue);
                PutModule("RealName = " + sValue);
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "bindhost") {
            if (!pUser->DenySetBindHost() || GetUser()->IsAdmin()) {
                if (sValue.Equals(pUser->GetBindHost())) {
                    PutModule(t_s("This bind host is already set!"));
                    return;
                }

                pUser->SetBindHost(sValue);
                PutModule("BindHost = " + sValue);
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "multiclients") {
            bool b = sValue.ToBool();
            pUser->SetMultiClients(b);
            PutModule("MultiClients = " + CString(b));
        } else if (sVar == "denyloadmod") {
            if (GetUser()->IsAdmin()) {
                bool b = sValue.ToBool();
                pUser->SetDenyLoadMod(b);
                PutModule("DenyLoadMod = " + CString(b));
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "denysetbindhost") {
            if (GetUser()->IsAdmin()) {
                bool b = sValue.ToBool();
                pUser->SetDenySetBindHost(b);
                PutModule("DenySetBindHost = " + CString(b));
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "denysetident") {
            if (GetUser()->IsAdmin()) {
                bool b = sValue.ToBool();
                pUser->SetDenySetIdent(b);
                PutModule("DenySetIdent = " + CString(b));
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "denysetnetwork") {
            if (GetUser()->IsAdmin()) {
                bool b = sValue.ToBool();
                pUser->SetDenySetNetwork(b);
                PutModule("DenySetNetwork = " + CString(b));
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "denysetrealname") {
            if (GetUser()->IsAdmin()) {
                bool b = sValue.ToBool();
                pUser->SetDenySetRealName(b);
                PutModule("DenySetRealName = " + CString(b));
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "denysetquitmsg") {
            if (GetUser()->IsAdmin()) {
                bool b = sValue.ToBool();
                pUser->SetDenySetQuitMsg(b);
                PutModule("DenySetQuitMsg = " + CString(b));
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "denysetctcpreplies") {
            if (GetUser()->IsAdmin()) {
                bool b = sValue.ToBool();
                pUser->SetDenySetCTCPReplies(b);
                PutModule("DenySetCTCPReplies = " + CString(b));
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "defaultchanmodes") {
            pUser->SetDefaultChanModes(sValue);
            PutModule("DefaultChanModes = " + sValue);
        } else if (sVar == "quitmsg") {
            if (!pUser->DenySetQuitMsg() || GetUser()->IsAdmin()) {
                pUser->SetQuitMsg(sValue);
                PutModule("QuitMsg = " + sValue);
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "chanbuffersize" || sVar == "buffercount") {
            unsigned int i = sValue.ToUInt();
            // Admins don't have to honour the buffer limit
            if (pUser->SetChanBufferSize(i, GetUser()->IsAdmin())) {
                PutModule("ChanBufferSize = " + sValue);
            } else {
                PutModule(t_f("Setting failed, limit for buffer size is {1}")(
                    CString(CZNC::Get().GetMaxBufferSize())));
            }
        } else if (sVar == "querybuffersize") {
            unsigned int i = sValue.ToUInt();
            // Admins don't have to honour the buffer limit
            if (pUser->SetQueryBufferSize(i, GetUser()->IsAdmin())) {
                PutModule("QueryBufferSize = " + sValue);
            } else {
                PutModule(t_f("Setting failed, limit for buffer size is {1}")(
                    CString(CZNC::Get().GetMaxBufferSize())));
            }
        } else if (sVar == "keepbuffer") {
            // XXX compatibility crap, added in 0.207
            bool b = !sValue.ToBool();
            pUser->SetAutoClearChanBuffer(b);
            PutModule("AutoClearChanBuffer = " + CString(b));
        } else if (sVar == "autoclearchanbuffer") {
            bool b = sValue.ToBool();
            pUser->SetAutoClearChanBuffer(b);
            PutModule("AutoClearChanBuffer = " + CString(b));
        } else if (sVar == "autoclearquerybuffer") {
            bool b = sValue.ToBool();
            pUser->SetAutoClearQueryBuffer(b);
            PutModule("AutoClearQueryBuffer = " + CString(b));
        } else if (sVar == "password") {
            const CString sSalt = CUtils::GetSalt();
            const CString sHash = CUser::SaltedHash(sValue, sSalt);
            pUser->SetPass(sHash, CUser::HASH_DEFAULT, sSalt);
            PutModule(t_s("Password has been changed!"));
        } else if (sVar == "maxjoins") {
            unsigned int i = sValue.ToUInt();
            pUser->SetMaxJoins(i);
            PutModule("MaxJoins = " + CString(pUser->MaxJoins()));
        } else if (sVar == "notraffictimeout") {
            unsigned int i = sValue.ToUInt();
            if (i < 30) {
                PutModule(t_s("Timeout can't be less than 30 seconds!"));
            } else {
                pUser->SetNoTrafficTimeout(i);
                PutModule("NoTrafficTimeout = " +
                          CString(pUser->GetNoTrafficTimeout()));
            }
        } else if (sVar == "maxnetworks") {
            if (GetUser()->IsAdmin()) {
                unsigned int i = sValue.ToUInt();
                pUser->SetMaxNetworks(i);
                PutModule("MaxNetworks = " + sValue);
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "maxquerybuffers") {
            unsigned int i = sValue.ToUInt();
            pUser->SetMaxQueryBuffers(i);
            PutModule("MaxQueryBuffers = " + sValue);
        } else if (sVar == "jointries") {
            unsigned int i = sValue.ToUInt();
            pUser->SetJoinTries(i);
            PutModule("JoinTries = " + CString(pUser->JoinTries()));
        } else if (sVar == "timezone") {
            pUser->SetTimezone(sValue);
            PutModule("Timezone = " + pUser->GetTimezone());
        } else if (sVar == "admin") {
            if (GetUser()->IsAdmin() && pUser != GetUser()) {
                bool b = sValue.ToBool();
                pUser->SetAdmin(b);
                PutModule("Admin = " + CString(pUser->IsAdmin()));
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "prependtimestamp") {
            bool b = sValue.ToBool();
            pUser->SetTimestampPrepend(b);
            PutModule("PrependTimestamp = " + CString(b));
        } else if (sVar == "appendtimestamp") {
            bool b = sValue.ToBool();
            pUser->SetTimestampAppend(b);
            PutModule("AppendTimestamp = " + CString(b));
        } else if (sVar == "authonlyviamodule") {
            if (GetUser()->IsAdmin()) {
                bool b = sValue.ToBool();
                pUser->SetAuthOnlyViaModule(b);
                PutModule("AuthOnlyViaModule = " + CString(b));
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "timestampformat") {
            pUser->SetTimestampFormat(sValue);
            PutModule("TimestampFormat = " + sValue);
        } else if (sVar == "dccbindhost") {
            if (!pUser->DenySetBindHost() || GetUser()->IsAdmin()) {
                pUser->SetDCCBindHost(sValue);
                PutModule("DCCBindHost = " + sValue);
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar == "statusprefix") {
            if (sVar.find_first_of(" \t\n") == CString::npos) {
                pUser->SetStatusPrefix(sValue);
                PutModule("StatusPrefix = " + sValue);
            } else {
                PutModule(t_s("That would be a bad idea!"));
            }
        }
#ifdef HAVE_I18N
        else if (sVar == "language") {
            auto mTranslations = CTranslationInfo::GetTranslations();
            // TODO: maybe stop special-casing English
            if (sValue == "en") {
                pUser->SetLanguage("");
                PutModule("Language is set to English");
            } else if (mTranslations.count(sValue)) {
                pUser->SetLanguage(sValue);
                PutModule("Language = " + sValue);
            } else {
                VCString vsCodes = {"en"};
                for (const auto it : mTranslations) {
                    vsCodes.push_back(it.first);
                }
                PutModule(t_f("Supported languages: {1}")(
                    CString(", ").Join(vsCodes.begin(), vsCodes.end())));
            }
        }
#endif
#ifdef HAVE_ICU
        else if (sVar == "clientencoding") {
            pUser->SetClientEncoding(sValue);
            PutModule("ClientEncoding = " + pUser->GetClientEncoding());
        }
#endif
        else
            PutModule(t_s("Error: Unknown variable"));
    }

    void GetNetwork(const CString& sLine) {
        const CString sVar = sLine.Token(1).AsLower();
        const CString sUsername = sLine.Token(2);
        const CString sNetwork = sLine.Token(3);

        CIRCNetwork* pNetwork = nullptr;
        CUser* pUser;

        if (sVar.empty()) {
            PutModule(t_s("Usage: GetNetwork <variable> [username] [network]"));
            return;
        }

        if (sUsername.empty()) {
            pUser = GetUser();
        } else {
            pUser = FindUser(sUsername);
        }

        if (!pUser) {
            return;
        }

        if (sNetwork.empty()) {
            if (pUser == GetUser()) {
                pNetwork = CModule::GetNetwork();
            } else {
                PutModule(
                    t_s("Error: A network must be specified to get another "
                        "users settings."));
                return;
            }

            if (!pNetwork) {
                PutModule(t_s("You are not currently attached to a network."));
                return;
            }
        } else {
            pNetwork = FindNetwork(pUser, sNetwork);
            if (!pNetwork) {
                PutModule(t_s("Error: Invalid network."));
                return;
            }
        }

        if (sVar.Equals("nick")) {
            PutModule("Nick = " + pNetwork->GetNick());
        } else if (sVar.Equals("altnick")) {
            PutModule("AltNick = " + pNetwork->GetAltNick());
        } else if (sVar.Equals("ident")) {
            PutModule("Ident = " + pNetwork->GetIdent());
        } else if (sVar.Equals("realname")) {
            PutModule("RealName = " + pNetwork->GetRealName());
        } else if (sVar.Equals("bindhost")) {
            PutModule("BindHost = " + pNetwork->GetBindHost());
        } else if (sVar.Equals("floodrate")) {
            PutModule("FloodRate = " + CString(pNetwork->GetFloodRate()));
        } else if (sVar.Equals("floodburst")) {
            PutModule("FloodBurst = " + CString(pNetwork->GetFloodBurst()));
        } else if (sVar.Equals("joindelay")) {
            PutModule("JoinDelay = " + CString(pNetwork->GetJoinDelay()));
#ifdef HAVE_ICU
        } else if (sVar.Equals("encoding")) {
            PutModule("Encoding = " + pNetwork->GetEncoding());
#endif
        } else if (sVar.Equals("quitmsg")) {
            PutModule("QuitMsg = " + pNetwork->GetQuitMsg());
        } else if (sVar.Equals("trustallcerts")) {
            PutModule("TrustAllCerts = " + CString(pNetwork->GetTrustAllCerts()));
        } else if (sVar.Equals("trustpki")) {
            PutModule("TrustPKI = " + CString(pNetwork->GetTrustPKI()));
        } else {
            PutModule(t_s("Error: Unknown variable"));
        }
    }

    void SetNetwork(const CString& sLine) {
        const CString sVar = sLine.Token(1).AsLower();
        const CString sUsername = sLine.Token(2);
        const CString sNetwork = sLine.Token(3);
        const CString sValue = sLine.Token(4, true);

        if (sValue.empty()) {
            PutModule(t_s(
                "Usage: SetNetwork <variable> <username> <network> <value>"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) {
            return;
        }

        CIRCNetwork* pNetwork = FindNetwork(pUser, sNetwork);
        if (!pNetwork) {
            return;
        }

        if (sVar.Equals("nick")) {
            pNetwork->SetNick(sValue);
            PutModule("Nick = " + pNetwork->GetNick());
        } else if (sVar.Equals("altnick")) {
            pNetwork->SetAltNick(sValue);
            PutModule("AltNick = " + pNetwork->GetAltNick());
        } else if (sVar.Equals("ident")) {
            if (!pUser->DenySetIdent() || GetUser()->IsAdmin()) {
                pNetwork->SetIdent(sValue);
                PutModule("Ident = " + pNetwork->GetIdent());
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar.Equals("realname")) {
            if (!pUser->DenySetRealName() || GetUser()->IsAdmin()) {
                pNetwork->SetRealName(sValue);
                PutModule("RealName = " + pNetwork->GetRealName());
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar.Equals("bindhost")) {
            if (!pUser->DenySetBindHost() || GetUser()->IsAdmin()) {
                if (sValue.Equals(pNetwork->GetBindHost())) {
                    PutModule(t_s("This bind host is already set!"));
                    return;
                }

                pNetwork->SetBindHost(sValue);
                PutModule("BindHost = " + sValue);
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar.Equals("floodrate")) {
            pNetwork->SetFloodRate(sValue.ToDouble());
            PutModule("FloodRate = " + CString(pNetwork->GetFloodRate()));
        } else if (sVar.Equals("floodburst")) {
            pNetwork->SetFloodBurst(sValue.ToUShort());
            PutModule("FloodBurst = " + CString(pNetwork->GetFloodBurst()));
        } else if (sVar.Equals("joindelay")) {
            pNetwork->SetJoinDelay(sValue.ToUShort());
            PutModule("JoinDelay = " + CString(pNetwork->GetJoinDelay()));
#ifdef HAVE_ICU
        } else if (sVar.Equals("encoding")) {
            pNetwork->SetEncoding(sValue);
            PutModule("Encoding = " + pNetwork->GetEncoding());
#endif
        } else if (sVar.Equals("quitmsg")) {
            if (!pUser->DenySetQuitMsg() || GetUser()->IsAdmin()) {
                pNetwork->SetQuitMsg(sValue);
                PutModule("QuitMsg = " + pNetwork->GetQuitMsg());
            } else {
                PutModule(t_s("Access denied!"));
            }
        } else if (sVar.Equals("trustallcerts")) {
            bool b = sValue.ToBool();
            pNetwork->SetTrustAllCerts(b);
            PutModule("TrustAllCerts = " + CString(b));
        } else if (sVar.Equals("trustpki")) {
            bool b = sValue.ToBool();
            pNetwork->SetTrustPKI(b);
            PutModule("TrustPKI = " + CString(b));
        } else {
            PutModule(t_s("Error: Unknown variable"));
        }
    }

    void AddChan(const CString& sLine) {
        const CString sUsername = sLine.Token(1);
        const CString sNetwork = sLine.Token(2);
        const CString sChan = sLine.Token(3);

        if (sChan.empty()) {
            PutModule(t_s("Usage: AddChan <username> <network> <channel>"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        CIRCNetwork* pNetwork = FindNetwork(pUser, sNetwork);
        if (!pNetwork) {
            return;
        }

        if (pNetwork->FindChan(sChan)) {
            PutModule(t_f("Error: User {1} already has a channel named {2}.")(
                sUsername, sChan));
            return;
        }

        CChan* pChan = new CChan(sChan, pNetwork, true);
        if (pNetwork->AddChan(pChan))
            PutModule(t_f("Channel {1} for user {2} added to network {3}.")(
                pChan->GetName(), sUsername, pNetwork->GetName()));
        else
            PutModule(t_f(
                "Could not add channel {1} for user {2} to network {3}, does "
                "it already exist?")(sChan, sUsername, pNetwork->GetName()));
    }

    void DelChan(const CString& sLine) {
        const CString sUsername = sLine.Token(1);
        const CString sNetwork = sLine.Token(2);
        const CString sChan = sLine.Token(3);

        if (sChan.empty()) {
            PutModule(t_s("Usage: DelChan <username> <network> <channel>"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        CIRCNetwork* pNetwork = FindNetwork(pUser, sNetwork);
        if (!pNetwork) {
            return;
        }

        std::vector<CChan*> vChans = pNetwork->FindChans(sChan);
        if (vChans.empty()) {
            PutModule(
                t_f("Error: User {1} does not have any channel matching [{2}] "
                    "in network {3}")(sUsername, sChan, pNetwork->GetName()));
            return;
        }

        VCString vsNames;
        for (const CChan* pChan : vChans) {
            const CString& sName = pChan->GetName();
            vsNames.push_back(sName);
            pNetwork->PutIRC("PART " + sName);
            pNetwork->DelChan(sName);
        }

        PutModule(t_p("Channel {1} is deleted from network {2} of user {3}",
                      "Channels {1} are deleted from network {2} of user {3}",
                      vsNames.size())(
            CString(", ").Join(vsNames.begin(), vsNames.end()),
            pNetwork->GetName(), sUsername));
    }

    void GetChan(const CString& sLine) {
        const CString sVar = sLine.Token(1).AsLower();
        CString sUsername = sLine.Token(2);
        CString sNetwork = sLine.Token(3);
        CString sChan = sLine.Token(4, true);

        if (sChan.empty()) {
            PutModule(
                t_s("Usage: GetChan <variable> <username> <network> <chan>"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        CIRCNetwork* pNetwork = FindNetwork(pUser, sNetwork);
        if (!pNetwork) {
            return;
        }

        std::vector<CChan*> vChans = pNetwork->FindChans(sChan);
        if (vChans.empty()) {
            PutModule(t_f("Error: No channels matching [{1}] found.")(sChan));
            return;
        }

        for (CChan* pChan : vChans) {
            if (sVar == "defmodes") {
                PutModule(pChan->GetName() + ": DefModes = " +
                          pChan->GetDefaultModes());
            } else if (sVar == "buffersize" || sVar == "buffer") {
                CString sValue(pChan->GetBufferCount());
                if (!pChan->HasBufferCountSet()) {
                    sValue += " (default)";
                }
                PutModule(pChan->GetName() + ": BufferSize = " + sValue);
            } else if (sVar == "inconfig") {
                PutModule(pChan->GetName() + ": InConfig = " +
                          CString(pChan->InConfig()));
            } else if (sVar == "keepbuffer") {
                // XXX compatibility crap, added in 0.207
                PutModule(pChan->GetName() + ": KeepBuffer = " +
                          CString(!pChan->AutoClearChanBuffer()));
            } else if (sVar == "autoclearchanbuffer") {
                CString sValue(pChan->AutoClearChanBuffer());
                if (!pChan->HasAutoClearChanBufferSet()) {
                    sValue += " (default)";
                }
                PutModule(pChan->GetName() + ": AutoClearChanBuffer = " +
                          sValue);
            } else if (sVar == "detached") {
                PutModule(pChan->GetName() + ": Detached = " +
                          CString(pChan->IsDetached()));
            } else if (sVar == "key") {
                PutModule(pChan->GetName() + ": Key = " + pChan->GetKey());
            } else {
                PutModule(t_s("Error: Unknown variable"));
                return;
            }
        }
    }

    void SetChan(const CString& sLine) {
        const CString sVar = sLine.Token(1).AsLower();
        CString sUsername = sLine.Token(2);
        CString sNetwork = sLine.Token(3);
        CString sChan = sLine.Token(4);
        CString sValue = sLine.Token(5, true);

        if (sValue.empty()) {
            PutModule(
                t_s("Usage: SetChan <variable> <username> <network> <chan> "
                    "<value>"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        CIRCNetwork* pNetwork = FindNetwork(pUser, sNetwork);
        if (!pNetwork) {
            return;
        }

        std::vector<CChan*> vChans = pNetwork->FindChans(sChan);
        if (vChans.empty()) {
            PutModule(t_f("Error: No channels matching [{1}] found.")(sChan));
            return;
        }

        for (CChan* pChan : vChans) {
            if (sVar == "defmodes") {
                pChan->SetDefaultModes(sValue);
                PutModule(pChan->GetName() + ": DefModes = " + sValue);
            } else if (sVar == "buffersize" || sVar == "buffer") {
                unsigned int i = sValue.ToUInt();
                if (sValue.Equals("-")) {
                    pChan->ResetBufferCount();
                    PutModule(pChan->GetName() + ": BufferSize = " +
                              CString(pChan->GetBufferCount()));
                } else if (pChan->SetBufferCount(i, GetUser()->IsAdmin())) {
                    // Admins don't have to honour the buffer limit
                    PutModule(pChan->GetName() + ": BufferSize = " + sValue);
                } else {
                    PutModule(
                        t_f("Setting failed, limit for buffer size is {1}")(
                            CString(CZNC::Get().GetMaxBufferSize())));
                    return;
                }
            } else if (sVar == "inconfig") {
                bool b = sValue.ToBool();
                pChan->SetInConfig(b);
                PutModule(pChan->GetName() + ": InConfig = " + CString(b));
            } else if (sVar == "keepbuffer") {
                // XXX compatibility crap, added in 0.207
                bool b = !sValue.ToBool();
                pChan->SetAutoClearChanBuffer(b);
                PutModule(pChan->GetName() + ": AutoClearChanBuffer = " +
                          CString(b));
            } else if (sVar == "autoclearchanbuffer") {
                if (sValue.Equals("-")) {
                    pChan->ResetAutoClearChanBuffer();
                } else {
                    bool b = sValue.ToBool();
                    pChan->SetAutoClearChanBuffer(b);
                }
                PutModule(pChan->GetName() + ": AutoClearChanBuffer = " +
                          CString(pChan->AutoClearChanBuffer()));
            } else if (sVar == "detached") {
                bool b = sValue.ToBool();
                if (pChan->IsDetached() != b) {
                    if (b)
                        pChan->DetachUser();
                    else
                        pChan->AttachUser();
                }
                PutModule(pChan->GetName() + ": Detached = " + CString(b));
            } else if (sVar == "key") {
                pChan->SetKey(sValue);
                PutModule(pChan->GetName() + ": Key = " + sValue);
            } else {
                PutModule(t_s("Error: Unknown variable"));
                return;
            }
        }
    }

    void ListUsers(const CString&) {
        if (!GetUser()->IsAdmin()) return;

        const map<CString, CUser*>& msUsers = CZNC::Get().GetUserMap();
        CTable Table;
        Table.AddColumn(t_s("Username", "listusers"));
        Table.AddColumn(t_s("Realname", "listusers"));
        Table.AddColumn(t_s("IsAdmin", "listusers"));
        Table.AddColumn(t_s("Nick", "listusers"));
        Table.AddColumn(t_s("AltNick", "listusers"));
        Table.AddColumn(t_s("Ident", "listusers"));
        Table.AddColumn(t_s("BindHost", "listusers"));

        for (const auto& it : msUsers) {
            Table.AddRow();
            Table.SetCell(t_s("Username", "listusers"), it.first);
            Table.SetCell(t_s("Realname", "listusers"),
                          it.second->GetRealName());
            if (!it.second->IsAdmin())
                Table.SetCell(t_s("IsAdmin", "listusers"), t_s("No"));
            else
                Table.SetCell(t_s("IsAdmin", "listusers"), t_s("Yes"));
            Table.SetCell(t_s("Nick", "listusers"), it.second->GetNick());
            Table.SetCell(t_s("AltNick", "listusers"), it.second->GetAltNick());
            Table.SetCell(t_s("Ident", "listusers"), it.second->GetIdent());
            Table.SetCell(t_s("BindHost", "listusers"),
                          it.second->GetBindHost());
        }

        PutModule(Table);
    }

    void AddUser(const CString& sLine) {
        if (!GetUser()->IsAdmin()) {
            PutModule(
                t_s("Error: You need to have admin rights to add new users!"));
            return;
        }

        const CString sUsername = sLine.Token(1), sPassword = sLine.Token(2);
        if (sPassword.empty()) {
            PutModule(t_s("Usage: AddUser <username> <password>"));
            return;
        }

        if (CZNC::Get().FindUser(sUsername)) {
            PutModule(t_f("Error: User {1} already exists!")(sUsername));
            return;
        }

        CUser* pNewUser = new CUser(sUsername);
        CString sSalt = CUtils::GetSalt();
        pNewUser->SetPass(CUser::SaltedHash(sPassword, sSalt),
                          CUser::HASH_DEFAULT, sSalt);

        CString sErr;
        if (!CZNC::Get().AddUser(pNewUser, sErr)) {
            delete pNewUser;
            PutModule(t_f("Error: User not added: {1}")(sErr));
            return;
        }

        PutModule(t_f("User {1} added!")(sUsername));
        return;
    }

    void DelUser(const CString& sLine) {
        if (!GetUser()->IsAdmin()) {
            PutModule(
                t_s("Error: You need to have admin rights to delete users!"));
            return;
        }

        const CString sUsername = sLine.Token(1, true);
        if (sUsername.empty()) {
            PutModule(t_s("Usage: DelUser <username>"));
            return;
        }

        CUser* pUser = CZNC::Get().FindUser(sUsername);

        if (!pUser) {
            PutModule(t_f("Error: User [{1}] does not exist!")(sUsername));
            return;
        }

        if (pUser == GetUser()) {
            PutModule(t_s("Error: You can't delete yourself!"));
            return;
        }

        if (!CZNC::Get().DeleteUser(pUser->GetUsername())) {
            // This can't happen, because we got the user from FindUser()
            PutModule(t_s("Error: Internal error!"));
            return;
        }

        PutModule(t_f("User {1} deleted!")(sUsername));
        return;
    }

    void CloneUser(const CString& sLine) {
        if (!GetUser()->IsAdmin()) {
            PutModule(
                t_s("Error: You need to have admin rights to add new users!"));
            return;
        }

        const CString sOldUsername = sLine.Token(1),
                      sNewUsername = sLine.Token(2, true);

        if (sOldUsername.empty() || sNewUsername.empty()) {
            PutModule(t_s("Usage: CloneUser <old username> <new username>"));
            return;
        }

        CUser* pOldUser = CZNC::Get().FindUser(sOldUsername);

        if (!pOldUser) {
            PutModule(t_f("Error: User [{1}] does not exist!")(sOldUsername));
            return;
        }

        CUser* pNewUser = new CUser(sNewUsername);
        CString sError;
        if (!pNewUser->Clone(*pOldUser, sError)) {
            delete pNewUser;
            PutModule(t_f("Error: Cloning failed: {1}")(sError));
            return;
        }

        if (!CZNC::Get().AddUser(pNewUser, sError)) {
            delete pNewUser;
            PutModule(t_f("Error: User not added: {1}")(sError));
            return;
        }

        PutModule(t_f("User {1} added!")(sNewUsername));
        return;
    }

    void AddNetwork(const CString& sLine) {
        CString sUser = sLine.Token(1);
        CString sNetwork = sLine.Token(2);
        CUser* pUser = GetUser();

        if (sNetwork.empty()) {
            sNetwork = sUser;
        } else {
            pUser = FindUser(sUser);
            if (!pUser) {
                return;
            }
        }

        if (sNetwork.empty()) {
            PutModule(t_s("Usage: AddNetwork [user] network"));
            return;
        }

        if (!GetUser()->IsAdmin() && pUser->DenySetNetwork()) {
            PutModule(t_s("Access denied!"));
            return;
        }

        if (!GetUser()->IsAdmin() && !pUser->HasSpaceForNewNetwork()) {
            PutStatus(
                t_s("Network number limit reached. Ask an admin to increase "
                    "the limit for you, or delete unneeded networks using /znc "
                    "DelNetwork <name>"));
            return;
        }

        if (pUser->FindNetwork(sNetwork)) {
            PutModule(
                t_f("Error: User {1} already has a network with the name {2}")(
                    pUser->GetUsername(), sNetwork));
            return;
        }

        CString sNetworkAddError;
        if (pUser->AddNetwork(sNetwork, sNetworkAddError)) {
            PutModule(t_f("Network {1} added to user {2}.")(
                sNetwork, pUser->GetUsername()));
        } else {
            PutModule(t_f(
                "Error: Network [{1}] could not be added for user {2}: {3}")(
                sNetwork, pUser->GetUsername(), sNetworkAddError));
        }
    }

    void DelNetwork(const CString& sLine) {
        CString sUser = sLine.Token(1);
        CString sNetwork = sLine.Token(2);
        CUser* pUser = GetUser();

        if (sNetwork.empty()) {
            sNetwork = sUser;
        } else {
            pUser = FindUser(sUser);
            if (!pUser) {
                return;
            }
        }

        if (sNetwork.empty()) {
            PutModule(t_s("Usage: DelNetwork [user] network"));
            return;
        }

        if (!GetUser()->IsAdmin() && pUser->DenySetNetwork()) {
            PutModule(t_s("Access denied!"));
            return;
        }

        CIRCNetwork* pNetwork = FindNetwork(pUser, sNetwork);
        if (!pNetwork) {
            return;
        }

        if (pNetwork == CModule::GetNetwork()) {
            PutModule(t_f(
                "The currently active network can be deleted via {1}status")(
                GetUser()->GetStatusPrefix()));
            return;
        }

        if (pUser->DeleteNetwork(sNetwork)) {
            PutModule(t_f("Network {1} deleted for user {2}.")(
                sNetwork, pUser->GetUsername()));
        } else {
            PutModule(
                t_f("Error: Network {1} could not be deleted for user {2}.")(
                    sNetwork, pUser->GetUsername()));
        }
    }

    void ListNetworks(const CString& sLine) {
        CString sUser = sLine.Token(1);
        CUser* pUser = GetUser();

        if (!sUser.empty()) {
            pUser = FindUser(sUser);
            if (!pUser) {
                return;
            }
        }

        const vector<CIRCNetwork*>& vNetworks = pUser->GetNetworks();

        CTable Table;
        Table.AddColumn(t_s("Network", "listnetworks"));
        Table.AddColumn(t_s("OnIRC", "listnetworks"));
        Table.AddColumn(t_s("IRC Server", "listnetworks"));
        Table.AddColumn(t_s("IRC User", "listnetworks"));
        Table.AddColumn(t_s("Channels", "listnetworks"));

        for (const CIRCNetwork* pNetwork : vNetworks) {
            Table.AddRow();
            Table.SetCell(t_s("Network", "listnetworks"), pNetwork->GetName());
            if (pNetwork->IsIRCConnected()) {
                Table.SetCell(t_s("OnIRC", "listnetworks"), t_s("Yes"));
                Table.SetCell(t_s("IRC Server", "listnetworks"),
                              pNetwork->GetIRCServer());
                Table.SetCell(t_s("IRC User", "listnetworks"),
                              pNetwork->GetIRCNick().GetNickMask());
                Table.SetCell(t_s("Channels", "listnetworks"),
                              CString(pNetwork->GetChans().size()));
            } else {
                Table.SetCell(t_s("OnIRC", "listnetworks"), t_s("No"));
            }
        }

        if (PutModule(Table) == 0) {
            PutModule(t_s("No networks"));
        }
    }

    void AddServer(const CString& sLine) {
        CString sUsername = sLine.Token(1);
        CString sNetwork = sLine.Token(2);
        CString sServer = sLine.Token(3, true);

        if (sServer.empty()) {
            PutModule(
                t_s("Usage: AddServer <username> <network> <server> [[+]port] "
                    "[password]"));
            if (GetUser()->IsAdmin()) {
                PutModule(t_s("Or: AddServer <username> <network> unix:[ssl:]/path/to/socket"));
            }
            PutModule(t_s("+ means SSL"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        if (!GetUser()->IsAdmin() && pUser->DenySetNetwork()) {
            PutModule(t_s("Access denied!"));
            return;
        }

        CIRCNetwork* pNetwork = FindNetwork(pUser, sNetwork);
        if (!pNetwork) {
            return;
        }

        CServer Server = CServer::Parse(sServer);
        if (Server.IsUnixSocket() && !GetUser()->IsAdmin()) {
            PutModule(t_s("Access denied!"));
            return;
        }

        if (pNetwork->AddServer(std::move(Server)))
            PutModule(t_f("Added IRC Server {1} to network {2} for user {3}.")(
                sServer, pNetwork->GetName(), pUser->GetUsername()));
        else
            PutModule(t_f(
                "Error: Could not add IRC server {1} to network {2} for user "
                "{3}.")(sServer, pNetwork->GetName(), pUser->GetUsername()));
    }

    void DelServer(const CString& sLine) {
        CString sUsername = sLine.Token(1);
        CString sNetwork = sLine.Token(2);
        CString sServer = sLine.Token(3, true);

        if (sServer.empty()) {
            PutModule(
                t_s("Usage: DelServer <username> <network> <server> [[+]port] "
                    "[password]"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        if (!GetUser()->IsAdmin() && pUser->DenySetNetwork()) {
            PutModule(t_s("Access denied!"));
            return;
        }

        CIRCNetwork* pNetwork = FindNetwork(pUser, sNetwork);
        if (!pNetwork) {
            return;
        }

        if (pNetwork->DelServer(CServer::Parse(sServer)))
            PutModule(
                t_f("Deleted IRC Server {1} from network {2} for user {3}.")(
                    sServer, pNetwork->GetName(), pUser->GetUsername()));
        else
            PutModule(
                t_f("Error: Could not delete IRC server {1} from network {2} "
                    "for user {3}.")(sServer, pNetwork->GetName(),
                                     pUser->GetUsername()));
    }

    void ReconnectUser(const CString& sLine) {
        CString sUsername = sLine.Token(1);
        CString sNetwork = sLine.Token(2);

        if (sNetwork.empty()) {
            PutModule(t_s("Usage: Reconnect <username> <network>"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) {
            return;
        }

        CIRCNetwork* pNetwork = FindNetwork(pUser, sNetwork);
        if (!pNetwork) {
            return;
        }

        CIRCSock* pIRCSock = pNetwork->GetIRCSock();
        // cancel connection attempt:
        if (pIRCSock && !pIRCSock->IsConnected()) {
            pIRCSock->Close();
        }
        // or close existing connection:
        else if (pIRCSock) {
            pIRCSock->Quit();
        }

        // then reconnect
        pNetwork->SetIRCConnectEnabled(true);

        PutModule(t_f("Queued network {1} of user {2} for a reconnect.")(
            pNetwork->GetName(), pUser->GetUsername()));
    }

    void DisconnectUser(const CString& sLine) {
        CString sUsername = sLine.Token(1);
        CString sNetwork = sLine.Token(2);

        if (sNetwork.empty()) {
            PutModule(t_s("Usage: Disconnect <username> <network>"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) {
            return;
        }

        CIRCNetwork* pNetwork = FindNetwork(pUser, sNetwork);
        if (!pNetwork) {
            return;
        }

        pNetwork->SetIRCConnectEnabled(false);
        PutModule(t_f("Closed IRC connection for network {1} of user {2}.")(
            pNetwork->GetName(), pUser->GetUsername()));
    }

    void ListCTCP(const CString& sLine) {
        CString sUsername = sLine.Token(1, true);

        if (sUsername.empty()) {
            sUsername = GetUser()->GetUsername();
        }
        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        const MCString& msCTCPReplies = pUser->GetCTCPReplies();
        CTable Table;
        Table.AddColumn(t_s("Request", "listctcp"));
        Table.AddColumn(t_s("Reply", "listctcp"));
        Table.SetStyle(CTable::ListStyle);
        for (const auto& it : msCTCPReplies) {
            Table.AddRow();
            Table.SetCell(t_s("Request", "listctcp"), it.first);
            Table.SetCell(t_s("Reply", "listctcp"), it.second);
        }

        if (Table.empty()) {
            PutModule(t_f("No CTCP replies for user {1} are configured")(
                pUser->GetUsername()));
        } else {
            PutModule(t_f("CTCP replies for user {1}:")(pUser->GetUsername()));
            PutModule(Table);
        }
    }

    void AddCTCP(const CString& sLine) {
        CString sUsername = sLine.Token(1);
        CString sCTCPRequest = sLine.Token(2);
        CString sCTCPReply = sLine.Token(3, true);

        if (sCTCPRequest.empty()) {
            sCTCPRequest = sUsername;
            sCTCPReply = sLine.Token(2, true);
            sUsername = GetUser()->GetUsername();
        }
        if (sCTCPRequest.empty()) {
            PutModule(t_s("Usage: AddCTCP [user] [request] [reply]"));
            PutModule(
                t_s("This will cause ZNC to reply to the CTCP instead of "
                    "forwarding it to clients."));
            PutModule(t_s(
                "An empty reply will cause the CTCP request to be blocked."));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        if (!GetUser()->IsAdmin() && pUser->DenySetCTCPReplies()) {
            PutModule(t_s("Access denied!"));
            return;
        }

        pUser->AddCTCPReply(sCTCPRequest, sCTCPReply);
        if (sCTCPReply.empty()) {
            PutModule(t_f("CTCP requests {1} to user {2} will now be blocked.")(
                sCTCPRequest.AsUpper(), pUser->GetUsername()));
        } else {
            PutModule(
                t_f("CTCP requests {1} to user {2} will now get reply: {3}")(
                    sCTCPRequest.AsUpper(), pUser->GetUsername(), sCTCPReply));
        }
    }

    void DelCTCP(const CString& sLine) {
        CString sUsername = sLine.Token(1);
        CString sCTCPRequest = sLine.Token(2, true);

        if (sCTCPRequest.empty()) {
            sCTCPRequest = sUsername;
            sUsername = GetUser()->GetUsername();
        }
        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        if (!GetUser()->IsAdmin() && pUser->DenySetCTCPReplies()) {
            PutModule(t_s("Access denied!"));
            return;
        }

        if (sCTCPRequest.empty()) {
            PutModule(t_s("Usage: DelCTCP [user] [request]"));
            return;
        }

        if (pUser->DelCTCPReply(sCTCPRequest)) {
            PutModule(t_f(
                "CTCP requests {1} to user {2} will now be sent to IRC clients")(
                sCTCPRequest.AsUpper(), pUser->GetUsername()));
        } else {
            PutModule(
                t_f("CTCP requests {1} to user {2} will be sent to IRC clients "
                    "(nothing has changed)")(sCTCPRequest.AsUpper(),
                                             pUser->GetUsername()));
        }
    }

    void LoadModuleFor(CModules& Modules, const CString& sModName,
                       const CString& sArgs, CModInfo::EModuleType eType,
                       CUser* pUser, CIRCNetwork* pNetwork) {
        if (pUser->DenyLoadMod() && !GetUser()->IsAdmin()) {
            PutModule(t_s("Loading modules has been disabled."));
            return;
        }

        CString sModRet;
        CModule* pMod = Modules.FindModule(sModName);
        if (!pMod) {
            if (!Modules.LoadModule(sModName, sArgs, eType, pUser, pNetwork,
                                    sModRet)) {
                PutModule(t_f("Error: Unable to load module {1}: {2}")(
                    sModName, sModRet));
            } else {
                PutModule(t_f("Loaded module {1}")(sModName));
            }
        } else if (pMod->GetArgs() != sArgs) {
            if (!Modules.ReloadModule(sModName, sArgs, pUser, pNetwork,
                                      sModRet)) {
                PutModule(t_f("Error: Unable to reload module {1}: {2}")(
                    sModName, sModRet));
            } else {
                PutModule(t_f("Reloaded module {1}")(sModName));
            }
        } else {
            PutModule(
                t_f("Error: Unable to load module {1} because it is already "
                    "loaded")(sModName));
        }
    }

    void LoadModuleForUser(const CString& sLine) {
        CString sUsername = sLine.Token(1);
        CString sModName = sLine.Token(2);
        CString sArgs = sLine.Token(3, true);

        if (sModName.empty()) {
            PutModule(t_s("Usage: LoadModule <username> <modulename> [args]"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        LoadModuleFor(pUser->GetModules(), sModName, sArgs,
                      CModInfo::UserModule, pUser, nullptr);
    }

    void LoadModuleForNetwork(const CString& sLine) {
        CString sUsername = sLine.Token(1);
        CString sNetwork = sLine.Token(2);
        CString sModName = sLine.Token(3);
        CString sArgs = sLine.Token(4, true);

        if (sModName.empty()) {
            PutModule(
                t_s("Usage: LoadNetModule <username> <network> <modulename> "
                    "[args]"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        CIRCNetwork* pNetwork = FindNetwork(pUser, sNetwork);
        if (!pNetwork) {
            return;
        }

        LoadModuleFor(pNetwork->GetModules(), sModName, sArgs,
                      CModInfo::NetworkModule, pUser, pNetwork);
    }

    void UnLoadModuleFor(CModules& Modules, const CString& sModName,
                         CUser* pUser) {
        if (pUser->DenyLoadMod() && !GetUser()->IsAdmin()) {
            PutModule(t_s("Loading modules has been disabled."));
            return;
        }

        if (Modules.FindModule(sModName) == this) {
            PutModule(t_f("Please use /znc unloadmod {1}")(sModName));
            return;
        }

        CString sModRet;
        if (!Modules.UnloadModule(sModName, sModRet)) {
            PutModule(t_f("Error: Unable to unload module {1}: {2}")(sModName,
                                                                     sModRet));
        } else {
            PutModule(t_f("Unloaded module {1}")(sModName));
        }
    }

    void UnLoadModuleForUser(const CString& sLine) {
        CString sUsername = sLine.Token(1);
        CString sModName = sLine.Token(2);

        if (sModName.empty()) {
            PutModule(t_s("Usage: UnloadModule <username> <modulename>"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        UnLoadModuleFor(pUser->GetModules(), sModName, pUser);
    }

    void UnLoadModuleForNetwork(const CString& sLine) {
        CString sUsername = sLine.Token(1);
        CString sNetwork = sLine.Token(2);
        CString sModName = sLine.Token(3);

        if (sModName.empty()) {
            PutModule(t_s(
                "Usage: UnloadNetModule <username> <network> <modulename>"));
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        CIRCNetwork* pNetwork = FindNetwork(pUser, sNetwork);
        if (!pNetwork) {
            return;
        }

        UnLoadModuleFor(pNetwork->GetModules(), sModName, pUser);
    }

    void ListModulesFor(CModules& Modules) {
        CTable Table;
        Table.AddColumn(t_s("Name", "listmodules"));
        Table.AddColumn(t_s("Arguments", "listmodules"));
        Table.SetStyle(CTable::ListStyle);

        for (const CModule* pMod : Modules) {
            Table.AddRow();
            Table.SetCell(t_s("Name", "listmodules"), pMod->GetModName());
            Table.SetCell(t_s("Arguments", "listmodules"), pMod->GetArgs());
        }

        PutModule(Table);
    }

    void ListModulesForUser(const CString& sLine) {
        CString sUsername = sLine.Token(1);

        if (sUsername.empty()) {
            PutModule("Usage: ListMods <username>");
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        if (pUser->GetModules().empty()) {
            PutModule(
                t_f("User {1} has no modules loaded.")(pUser->GetUsername()));
            return;
        }

        PutModule(t_f("Modules loaded for user {1}:")(pUser->GetUsername()));
        ListModulesFor(pUser->GetModules());
    }

    void ListModulesForNetwork(const CString& sLine) {
        CString sUsername = sLine.Token(1);
        CString sNetwork = sLine.Token(2);

        if (sNetwork.empty()) {
            PutModule("Usage: ListNetMods <username> <network>");
            return;
        }

        CUser* pUser = FindUser(sUsername);
        if (!pUser) return;

        CIRCNetwork* pNetwork = FindNetwork(pUser, sNetwork);
        if (!pNetwork) return;

        if (pNetwork->GetModules().empty()) {
            PutModule(t_f("Network {1} of user {2} has no modules loaded.")(
                pNetwork->GetName(), pUser->GetUsername()));
            return;
        }

        PutModule(t_f("Modules loaded for network {1} of user {2}:")(
            pNetwork->GetName(), pUser->GetUsername()));
        ListModulesFor(pNetwork->GetModules());
    }

  public:
    MODCONSTRUCTOR(CAdminMod) {
        AddCommand("Help", t_d("[command] [variable]"),
                   t_d("Prints help for matching commands and variables"),
                   [=](const CString& sLine) { PrintHelp(sLine); });
        AddCommand(
            "Get", t_d("<variable> [username]"),
            t_d("Prints the variable's value for the given or current user"),
            [=](const CString& sLine) { Get(sLine); });
        AddCommand("Set", t_d("<variable> <username> <value>"),
                   t_d("Sets the variable's value for the given user"),
                   [=](const CString& sLine) { Set(sLine); });
        AddCommand("GetNetwork", t_d("<variable> [username] [network]"),
                   t_d("Prints the variable's value for the given network"),
                   [=](const CString& sLine) { GetNetwork(sLine); });
        AddCommand("SetNetwork", t_d("<variable> <username> <network> <value>"),
                   t_d("Sets the variable's value for the given network"),
                   [=](const CString& sLine) { SetNetwork(sLine); });
        AddCommand("GetChan", t_d("<variable> [username] <network> <chan>"),
                   t_d("Prints the variable's value for the given channel"),
                   [=](const CString& sLine) { GetChan(sLine); });
        AddCommand("SetChan",
                   t_d("<variable> <username> <network> <chan> <value>"),
                   t_d("Sets the variable's value for the given channel"),
                   [=](const CString& sLine) { SetChan(sLine); });
        AddCommand("AddChan", t_d("<username> <network> <chan>"),
                   t_d("Adds a new channel"),
                   [=](const CString& sLine) { AddChan(sLine); });
        AddCommand("DelChan", t_d("<username> <network> <chan>"),
                   t_d("Deletes a channel"),
                   [=](const CString& sLine) { DelChan(sLine); });
        AddCommand("ListUsers", "", t_d("Lists users"),
                   [=](const CString& sLine) { ListUsers(sLine); });
        AddCommand("AddUser", t_d("<username> <password>"),
                   t_d("Adds a new user"),
                   [=](const CString& sLine) { AddUser(sLine); });
        AddCommand("DelUser", t_d("<username>"), t_d("Deletes a user"),
                   [=](const CString& sLine) { DelUser(sLine); });
        AddCommand("CloneUser", t_d("<old username> <new username>"),
                   t_d("Clones a user"),
                   [=](const CString& sLine) { CloneUser(sLine); });
        AddCommand("AddServer", t_d("<username> <network> <server>"),
                   t_d("Adds a new IRC server for the given or current user"),
                   [=](const CString& sLine) { AddServer(sLine); });
        AddCommand("DelServer", t_d("<username> <network> <server>"),
                   t_d("Deletes an IRC server from the given or current user"),
                   [=](const CString& sLine) { DelServer(sLine); });
        AddCommand("Reconnect", t_d("<username> <network>"),
                   t_d("Cycles the user's IRC server connection"),
                   [=](const CString& sLine) { ReconnectUser(sLine); });
        AddCommand("Disconnect", t_d("<username> <network>"),
                   t_d("Disconnects the user from their IRC server"),
                   [=](const CString& sLine) { DisconnectUser(sLine); });
        AddCommand("LoadModule", t_d("<username> <modulename> [args]"),
                   t_d("Loads a Module for a user"),
                   [=](const CString& sLine) { LoadModuleForUser(sLine); });
        AddCommand("UnLoadModule", t_d("<username> <modulename>"),
                   t_d("Removes a Module of a user"),
                   [=](const CString& sLine) { UnLoadModuleForUser(sLine); });
        AddCommand("ListMods", t_d("<username>"),
                   t_d("Get the list of modules for a user"),
                   [=](const CString& sLine) { ListModulesForUser(sLine); });
        AddCommand("LoadNetModule",
                   t_d("<username> <network> <modulename> [args]"),
                   t_d("Loads a Module for a network"),
                   [=](const CString& sLine) { LoadModuleForNetwork(sLine); });
        AddCommand(
            "UnLoadNetModule", t_d("<username> <network> <modulename>"),
            t_d("Removes a Module of a network"),
            [=](const CString& sLine) { UnLoadModuleForNetwork(sLine); });
        AddCommand("ListNetMods", t_d("<username> <network>"),
                   t_d("Get the list of modules for a network"),
                   [=](const CString& sLine) { ListModulesForNetwork(sLine); });
        AddCommand("ListCTCPs", t_d("<username>"),
                   t_d("List the configured CTCP replies"),
                   [=](const CString& sLine) { ListCTCP(sLine); });
        AddCommand("AddCTCP", t_d("<username> <ctcp> [reply]"),
                   t_d("Configure a new CTCP reply"),
                   [=](const CString& sLine) { AddCTCP(sLine); });
        AddCommand("DelCTCP", t_d("<username> <ctcp>"),
                   t_d("Remove a CTCP reply"),
                   [=](const CString& sLine) { DelCTCP(sLine); });

        // Network commands
        AddCommand("AddNetwork", t_d("[username] <network>"),
                   t_d("Add a network for a user"),
                   [=](const CString& sLine) { AddNetwork(sLine); });
        AddCommand("DelNetwork", t_d("[username] <network>"),
                   t_d("Delete a network for a user"),
                   [=](const CString& sLine) { DelNetwork(sLine); });
        AddCommand("ListNetworks", t_d("[username]"),
                   t_d("List all networks for a user"),
                   [=](const CString& sLine) { ListNetworks(sLine); });
    }

    ~CAdminMod() override {}
};

template <>
void TModInfo<CAdminMod>(CModInfo& Info) {
    Info.SetWikiPage("controlpanel");
}

USERMODULEDEFS(CAdminMod,
               t_s("Dynamic configuration through IRC. Allows editing only "
               "yourself if you're not ZNC admin."))
