/* * InspIRCd -- Internet Relay Chat Daemon * * Copyright (C) 2026 Sadie Powell * * This file is part of InspIRCd. InspIRCd is free software: you can * redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 2. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "inspircd.h" #include "inspircd.h" #include "clientprotocolmsg.h" #include "numerichelper.h" #include "utility/container.h" struct SharedData final { // A reference to the +k user mode from the servicdes. UserModeReference servprotectmode; // Whether to sign messages with the executing operator's nick. bool sign = true; SharedData(const WeakModulePtr& mod) : servprotectmode(mod, "protect") { } }; class CommandSAJoin final : public Command { private: SharedData& data; public: CommandSAJoin(const WeakModulePtr& mod, SharedData& sd) : Command(mod, "SAJOIN", 1) , data(sd) { accepts_multiple_targets = true; access_needed = CmdAccess::OPERATOR; syntax = { "[] [,]+" }; translation = { TranslateType::NICK, TranslateType::TEXT }; } CmdResult Handle(User* user, const Params& parameters) override { const unsigned int channelindex = (parameters.size() > 1) ? 1 : 0; if (CommandParser::LoopCall(user, this, parameters, channelindex)) return CmdResult::FAILURE; User* target = user; if (parameters.size() > 1) { const auto& targetnick = parameters[0]; target = ServerInstance->Users.Find(targetnick, true); if (!target) { user->WriteRemoteNumeric(Numerics::NoSuchNick(targetnick)); return CmdResult::FAILURE; } } if (user != target) { // Force joining another user requires a special permission. if (!user->HasPrivPermission("users/sajoin-others")) { user->WriteRemoteNumeric(Numerics::NoPrivileges("your server operator account does not have the users/sajoin-others privilege")); return CmdResult::FAILURE; } if (target->IsModeSet(data.servprotectmode)) { user->WriteRemoteNumeric(Numerics::NoPrivileges("you can not use the {} command on a protected service", this->service_name)); return CmdResult::FAILURE; } } const auto& targetchan = parameters[channelindex]; if (user->IsLocal() && !ServerInstance->Channels.IsChannel(targetchan)) { user->WriteRemoteNumeric(ERR_BADCHANMASK, targetchan, "Invalid channel name"); return CmdResult::FAILURE; } auto* chan = ServerInstance->Channels.Find(targetchan); if (chan && chan->HasUser(target)) { user->WriteRemoteNumeric(ERR_USERONCHANNEL, target->nick, chan->name, "is already on channel"); return CmdResult::FAILURE; } auto* ltarget = target->AsLocal(); if (!ltarget) return CmdResult::SUCCESS; // User will be handled by their own server. auto* memb = Channel::JoinUser(ltarget, targetchan, true); if (!memb) { user->WriteRemoteNotice("*** {}: could not join {} to {}.", this->service_name, target->nick, targetchan); return CmdResult::FAILURE; } ServerInstance->SNO.WriteGlobalSno('a', "{} ({}) used {} to make {} join {}", user->nick, user->oper->GetName(), this->service_name, target->nick, memb->chan->name); return CmdResult::SUCCESS; } RouteDescriptor GetRouting(User* user, const Params& parameters) override { return ROUTE_OPT_UCAST(parameters[0]); } }; class CommandSAKick final : public Command { private: SharedData& data; public: CommandSAKick(const WeakModulePtr& mod, SharedData& sd) : Command(mod, "SAKICK", 2, 3) , data(sd) { access_needed = CmdAccess::OPERATOR; allow_empty_last_param = true; syntax = { " [:]" }; translation = { TranslateType::TEXT, TranslateType::NICK, TranslateType::TEXT }; } CmdResult Handle(User* user, const Params& parameters) override { const auto& targetnick = parameters[1]; auto* target = ServerInstance->Users.Find(targetnick, true); if (!target) { user->WriteRemoteNumeric(Numerics::NoSuchNick(targetnick)); return CmdResult::FAILURE; } if (target->IsModeSet(data.servprotectmode)) { user->WriteRemoteNumeric(Numerics::NoPrivileges("you can not use the {} command on a protected service", this->service_name)); return CmdResult::FAILURE; } const auto& targetchan = parameters[1]; auto* chan = ServerInstance->Channels.Find(targetchan); if (!chan) { user->WriteRemoteNumeric(Numerics::NoSuchChannel(targetchan)); return CmdResult::FAILURE; } if (!chan->HasUser(target)) { user->WriteRemoteNumeric(ERR_USERNOTINCHANNEL, target->nick, chan->name, "They are not on that channel"); return CmdResult::FAILURE; } if (!target->IsLocal()) return CmdResult::SUCCESS; // User will be handled by their own server. const std::string reason(parameters.size() > 2 ? parameters.back() : target->nick, 0, ServerInstance->Config->Limits.MaxKick); chan->KickUser(user, target, data.sign ? FMT::format("{} ({})", reason, user->nick) : reason); ServerInstance->SNO.WriteGlobalSno('a', "{} ({}) used {} to kick {} from {} ({})", user->nick, user->oper->GetName(), this->service_name, target->nick, chan->name, reason); return CmdResult::SUCCESS; } RouteDescriptor GetRouting(User* user, const Params& parameters) override { return ROUTE_OPT_UCAST(parameters[1]); } }; class CommandSAMode final : public Command { private: SharedData& data; bool logged = false; public: bool active = false; CommandSAMode(const WeakModulePtr& mod, SharedData& sd) : Command(mod, "SAMODE", 2) , data(sd) { access_needed = CmdAccess::OPERATOR; syntax = { " (+|-) []" }; } CmdResult Handle(User* user, const Params& parameters) override { const auto& targetstr = parameters[0]; if (!ServerInstance->Channels.IsPrefix(targetstr[0])) { auto* target = ServerInstance->Users.FindNick(targetstr, true); if (!target) { user->WriteNumeric(Numerics::NoSuchNick(targetstr)); return CmdResult::FAILURE; } if (target->IsModeSet(data.servprotectmode)) { user->WriteRemoteNumeric(Numerics::NoPrivileges("you can not use the {} command on a protected service", this->service_name)); return CmdResult::FAILURE; } // Changing the modes of another user requires a special permission. if (target != user && !user->HasPrivPermission("users/samode-usermodes")) { user->WriteRemoteNumeric(Numerics::NoPrivileges("your server operator account does not have the users/samode-usermodes privilege")); return CmdResult::FAILURE; } } // XXX: Make ModeParser clear LastParse Modes::ChangeList emptychangelist; ServerInstance->Modes.ProcessSingle(ServerInstance->FakeClient, nullptr, ServerInstance->FakeClient, emptychangelist); this->logged = false; this->active = true; ServerInstance->Parser.CallHandler("MODE", parameters, user); this->active = false; if (!this->logged) { // If we haven't logged anything yet then the client queried the list of a listmode // (e.g. /SAMODE #chan b), which was handled internally by the MODE command handler. // // Viewing the modes of a user or a channel could also result in this, but that is not // possible with /SAMODE because we require at least 2 parameters. this->LogUsage(user, targetstr, insp::join(insp::iterator_range(parameters.begin() + 1, parameters.end()))); } return CmdResult::SUCCESS; } void LogUsage(const User* user, const std::string& target, const std::string& modes) { this->logged = true; ServerInstance->SNO.WriteGlobalSno('a', "{} ({}) used {} to change the modes of {} to {}", user->nick, user->oper->GetName(), this->service_name, target, modes); } }; class CommandSANick final : public Command { private: SharedData& data; public: CommandSANick(const WeakModulePtr& mod, SharedData& sd) : Command(mod, "SANICK", 2) , data(sd) { access_needed = CmdAccess::OPERATOR; allow_empty_last_param = true; syntax = { " " }; translation = { TranslateType::NICK, TranslateType::TEXT }; } CmdResult Handle(User* user, const Params& parameters) override { const auto& targetnick = parameters[0]; auto* target = ServerInstance->Users.Find(targetnick, true); if (!target) { user->WriteRemoteNumeric(Numerics::NoSuchNick(targetnick)); return CmdResult::FAILURE; } if (target->IsModeSet(data.servprotectmode)) { user->WriteRemoteNumeric(Numerics::NoPrivileges("you can not use the {} command on a protected service", this->service_name)); return CmdResult::FAILURE; } auto targetnewnick = parameters[1]; if (targetnewnick.empty()) { user->WriteRemoteNumeric(ERR_NONICKNAMEGIVEN, "No nickname given"); return CmdResult::FAILURE; } if (targetnewnick == "0") targetnewnick = target->uuid; else if (!ServerInstance->Users.IsNick(targetnewnick)) { user->WriteRemoteNumeric(ERR_ERRONEUSNICKNAME, targetnewnick, "Erroneous nickname"); return CmdResult::FAILURE; } if (ServerInstance->Users.FindNick(targetnewnick)) { user->WriteRemoteNumeric(ERR_NICKNAMEINUSE, targetnewnick, "Nickname is already in use."); return CmdResult::FAILURE; } if (!target->IsLocal()) return CmdResult::SUCCESS; // User will be handled by their own server. if (!target->ChangeNick(targetnewnick)) { user->WriteRemoteNotice("*** {}: could not change the nick of {} to {}.", this->service_name, targetnick, targetnewnick); return CmdResult::FAILURE; } ServerInstance->SNO.WriteGlobalSno('a', "{} ({}) used {} to change the nick of {} to {}", user->nick, user->oper->GetName(), this->service_name, targetnick, targetnewnick); return CmdResult::SUCCESS; } RouteDescriptor GetRouting(User* user, const Params& parameters) override { return ROUTE_OPT_UCAST(parameters[0]); } }; class CommandSAPart final : public Command { private: SharedData& data; public: CommandSAPart(const WeakModulePtr& mod, SharedData& sd) : Command(mod, "SAPART", 2, 3) , data(sd) { accepts_multiple_targets = true; access_needed = CmdAccess::OPERATOR; allow_empty_last_param = true; syntax = { " [,]+ [:]" }; translation = { TranslateType::NICK, TranslateType::TEXT, TranslateType::TEXT }; } CmdResult Handle(User* user, const Params& parameters) override { if (CommandParser::LoopCall(user, this, parameters, 1)) return CmdResult::FAILURE; const auto& targetnick = parameters[0]; auto* target = ServerInstance->Users.Find(targetnick, true); if (!target) { user->WriteRemoteNumeric(Numerics::NoSuchNick(targetnick)); return CmdResult::FAILURE; } if (target->IsModeSet(data.servprotectmode)) { user->WriteRemoteNumeric(Numerics::NoPrivileges("you can not use the {} command on a protected service", this->service_name)); return CmdResult::FAILURE; } const auto& targetchan = parameters[0]; auto* chan = ServerInstance->Channels.Find(targetchan); if (!chan) { user->WriteRemoteNumeric(Numerics::NoSuchChannel(targetchan)); return CmdResult::FAILURE; } if (!target->IsLocal()) return CmdResult::SUCCESS; // User will be handled by their own server. const auto& reason = parameters.size() > 2 ? parameters.back() : ""; if (!chan->PartUser(target, data.sign ? FMT::format("{}{}({})", reason, reason.empty() ? "" : " ", user->nick) : reason)) { user->WriteRemoteNumeric(ERR_USERNOTINCHANNEL, target->nick, chan->name, "They are not on that channel"); return CmdResult::FAILURE; } ServerInstance->SNO.WriteGlobalSno('a', "{} ({}) used {} to part {} from {} ({})", user->nick, user->oper->GetName(), this->service_name, target->nick, chan->name, reason); return CmdResult::FAILURE; } RouteDescriptor GetRouting(User* user, const Params& parameters) override { return ROUTE_OPT_UCAST(parameters[0]); } }; class CommandSAQuit final : public Command { private: SharedData& data; public: CommandSAQuit(const WeakModulePtr& mod, SharedData& sd) : Command(mod, "SAQUIT", 2, 2) , data(sd) { access_needed = CmdAccess::OPERATOR; allow_empty_last_param = true; syntax = { " :" }; translation = { TranslateType::NICK, TranslateType::TEXT }; } CmdResult Handle(User* user, const Params& parameters) override { const auto& targetnick = parameters[0]; auto* target = ServerInstance->Users.Find(targetnick, true); if (!target) { user->WriteRemoteNumeric(Numerics::NoSuchNick(targetnick)); return CmdResult::FAILURE; } if (target->IsModeSet(data.servprotectmode)) { user->WriteRemoteNumeric(Numerics::NoPrivileges("you can not use the {} command on a protected service", this->service_name)); return CmdResult::FAILURE; } if (!target->IsLocal()) return CmdResult::SUCCESS; // User will be handled by their own server. const auto& reason = parameters.size() > 2 ? parameters.back() : ""; ServerInstance->Users.QuitUser(target, data.sign ? FMT::format("{}{}({})", reason, reason.empty() ? "" : " ", user->nick) : reason); ServerInstance->SNO.WriteGlobalSno('a', "{} ({}) used {} to quit {} ({})", user->nick, user->oper->GetName(), this->service_name, target->nick, reason); return CmdResult::FAILURE; } RouteDescriptor GetRouting(User* user, const Params& parameters) override { return ROUTE_OPT_UCAST(parameters[0]); } }; class CommandSATopic final : public Command { public: CommandSATopic(const WeakModulePtr& mod) : Command(mod, "SATOPIC", 2, 2) { access_needed = CmdAccess::OPERATOR; allow_empty_last_param = true; syntax = { " :" }; } CmdResult Handle(User* user, const Params& parameters) override { auto* target = ServerInstance->Channels.Find(parameters[0]); if (!target) { user->WriteRemoteNumeric(Numerics::NoSuchChannel(parameters[0])); return CmdResult::FAILURE; } const std::string topic(parameters[1], 0, ServerInstance->Config->Limits.MaxTopic); if (target->topic == topic) { user->WriteRemoteNotice("*** {}: The topic on {} is already what you are trying to change it to.", this->service_name, target->name); return CmdResult::SUCCESS; } target->SetTopic(user, topic, ServerInstance->Time(), nullptr); ServerInstance->SNO.WriteGlobalSno('a', "{} ({}) used {} to change the topic of {} ({})", user->nick, user->oper->GetName(), this->service_name, target->name, topic); return CmdResult::SUCCESS; } }; class ModuleSACommands final : public Module { private: SharedData data; CommandSAJoin cmdsajoin; CommandSAKick cmdsakick; CommandSAMode cmdsamode; CommandSANick cmdsanick; CommandSAPart cmdsapart; CommandSAQuit cmdsaquit; CommandSATopic cmdsatopic; public: ModuleSACommands() : Module(VF_VENDOR, "Adds various server operator-only versions of regular commands.") , data(weak_from_this()) , cmdsajoin(weak_from_this(), data) , cmdsakick(weak_from_this(), data) , cmdsamode(weak_from_this(), data) , cmdsanick(weak_from_this(), data) , cmdsapart(weak_from_this(), data) , cmdsaquit(weak_from_this(), data) , cmdsatopic(weak_from_this()) { } void ReadConfig(ConfigStatus& status) override { const auto& tag = ServerInstance->Config->ConfValue("sacommands"); data.sign = tag->getBool("sign", true); } void Prioritize() override { ServerInstance->Modules.SetPriority(shared_from_this(), I_OnRawMode, PRIORITY_BEFORE, "disable"); ServerInstance->Modules.SetPriority(shared_from_this(), I_OnPreMode, PRIORITY_BEFORE, "override"); } ModResult OnPreMode(User* source, User* dest, Channel* channel, Modes::ChangeList& modes) override { if (cmdsamode.active) return MOD_RES_ALLOW; return MOD_RES_PASSTHRU; } void OnMode(User* user, User* destuser, Channel* destchan, const Modes::ChangeList& modes, ModeParser::ModeProcessFlag processflags) override { if (!cmdsamode.active) return; auto modestr = ClientProtocol::Messages::Mode::ToModeLetters(modes); for (const auto& item : modes.getlist()) { if (!item.param.empty()) modestr.append(1, ' ').append(item.param); } cmdsamode.LogUsage(user, destuser ? destuser->nick : destchan->name, modestr); } }; MODULE_INIT(ModuleSACommands)