Created
November 15, 2025 21:26
-
-
Save TehPeGaSuS/335067bfc8269eea472c8b0fcf239d6c to your computer and use it in GitHub Desktop.
Ignore module for ZNC, based on srd424 work (https://codeberg.org/srd424/modignore.git)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* Based on srd424 ignore module (https://codeberg.org/srd424/modignore.git) | |
| * Merged ignore.h and ignore.cpp for simplicity of use | |
| */ | |
| #include <znc/User.h> | |
| #include <znc/Modules.h> | |
| #include <znc/znc.h> | |
| #include <regex.h> | |
| #include <vector> | |
| #include <string> | |
| #include <bitset> | |
| #include <iostream> | |
| #include <stdexcept> | |
| const int NUM_MODES = 12; | |
| const char MODES[NUM_MODES+1] = "mMaAnNcCjpqk"; | |
| class MatcherError : public std::runtime_error { | |
| public: | |
| MatcherError(const std::string& err) : std::runtime_error(err) {} | |
| }; | |
| class Matcher { | |
| protected: | |
| std::bitset<NUM_MODES> IgnoreModes; | |
| public: | |
| Matcher(const CString& modes); | |
| virtual ~Matcher() {} | |
| CString Modes() const; | |
| std::string Bits() const; | |
| bool operator ==(const Matcher& other) const; | |
| virtual bool Match(CNick& nick, const CString& line, int mode) const = 0; | |
| virtual CString String() const = 0; | |
| virtual CString Data() const = 0; | |
| virtual CString Type() const = 0; | |
| }; | |
| class HostMatcher : public Matcher { | |
| protected: | |
| CString Mask; | |
| public: | |
| HostMatcher(const CString& modes, const CString& mask); | |
| virtual ~HostMatcher() {} | |
| virtual bool Match(CNick& nick, const CString& line, int mode) const; | |
| virtual CString String() const; | |
| virtual CString Data() const; | |
| virtual CString Type() const; | |
| }; | |
| class RegexMatcher : public Matcher { | |
| protected: | |
| regex_t Regex; | |
| CString Pattern; | |
| public: | |
| RegexMatcher(const CString& modes, const CString& re); | |
| ~RegexMatcher(); | |
| virtual bool Match(CNick& nick, const CString& line, int mode) const; | |
| virtual CString String() const; | |
| virtual CString Data() const; | |
| virtual CString Type() const; | |
| }; | |
| // container because a std::vector<> can't contain my Matchers (abstract class | |
| // cannot be used for templates) | |
| typedef struct { | |
| Matcher* m; | |
| } IgnoreEntry; | |
| enum IMode { | |
| ModeMsg, | |
| ModePrivMsg, | |
| ModeAction, | |
| ModePrivAction, | |
| ModeNotice, | |
| ModePrivNotice, | |
| ModeCTCP, | |
| ModePrivCTCP, | |
| ModeJoin, | |
| ModePart, | |
| ModeQuit, | |
| ModeNick | |
| }; | |
| class ModIgnore : public CModule { | |
| protected: | |
| std::vector<IgnoreEntry> IgnoreList; | |
| public: | |
| virtual ~ModIgnore(); | |
| virtual bool OnLoad(const CString& args, CString& message); | |
| // Commands exposed to the user | |
| void CmdAddHostMatcher(const CString& line); | |
| void CmdAddRegexMatcher(const CString& line); | |
| void CmdDelIgnore(const CString& line); | |
| void CmdList(const CString& line); | |
| void CmdClear(const CString& line); | |
| // ZNC hook overrides | |
| virtual EModRet OnChanMsg(CNick& nick, CChan& chan, CString& message); | |
| virtual EModRet OnPrivMsg(CNick& nick, CString& message); | |
| virtual EModRet OnChanAction(CNick& nick, CChan& chan, CString& message); | |
| virtual EModRet OnPrivAction(CNick& nick, CString& message); | |
| virtual EModRet OnChanNotice(CNick& nick, CChan& chan, CString& message); | |
| virtual EModRet OnPrivNotice(CNick& nick, CString& message); | |
| virtual EModRet OnChanCTCP(CNick& nick, CChan& chan, CString& message); | |
| virtual EModRet OnPrivCTCP(CNick& nick, CString& message); | |
| virtual EModRet OnRawMessage(CMessage &message); | |
| MODCONSTRUCTOR(ModIgnore) { | |
| AddHelpCommand(); | |
| AddCommand("AddHost", static_cast<CModCommand::ModCmdFunc>(&ModIgnore::CmdAddHostMatcher), | |
| "[mMaAnNcCjpqk] <nick!user@host>", "Ignore a hostmask from [m]essage, [a]ction, [n]otice, [c]tcp, [j]oins, [p]arts, [q]uits, nic[k]; uppercase = private"); | |
| AddCommand("AddPattern",static_cast<CModCommand::ModCmdFunc>(&ModIgnore::CmdAddRegexMatcher), | |
| "[mMaAnNcCjpqk] <regex>", "Ignore text matching a regular expression"); | |
| AddCommand("Del", static_cast<CModCommand::ModCmdFunc>(&ModIgnore::CmdDelIgnore), | |
| "<n>", "Remove an ignore entry by index"); | |
| AddCommand("List", static_cast<CModCommand::ModCmdFunc>(&ModIgnore::CmdList), | |
| "", "Display the ignore list"); | |
| AddCommand("Clear", static_cast<CModCommand::ModCmdFunc>(&ModIgnore::CmdClear), | |
| "", "Clear all ignore entries"); | |
| } | |
| private: | |
| void addIgnore(IgnoreEntry ignore); | |
| EModRet check(CNick& nick, CString& message, int mode); | |
| void cleanup(); | |
| }; | |
| using namespace std; | |
| Matcher::Matcher(const CString& input_modes) { | |
| if (input_modes.empty()) { | |
| IgnoreModes = bitset<NUM_MODES>(0); | |
| } else { | |
| int sz = (int)input_modes.length(); | |
| char c0 = input_modes[0]; | |
| if (c0 == '1' || c0 == '0') { | |
| // this means it's the third format revision | |
| // make use of bitset constructor | |
| string modes(input_modes); | |
| if (sz != NUM_MODES) { | |
| modes += string(NUM_MODES-sz, '0'); | |
| } | |
| IgnoreModes = bitset<NUM_MODES>(modes); | |
| } else { | |
| // this means it's the second format revision | |
| // (man, I can imagine how code can get messy real fast trying to | |
| // support older versions) | |
| bool bad; | |
| for (int ch = 0; ch < sz; ch++) { | |
| bad = true; | |
| for (int mode = 0; mode < NUM_MODES; mode++) { | |
| if (MODES[mode] == input_modes[ch]) { | |
| IgnoreModes.set(mode); | |
| bad = false; | |
| break; | |
| } | |
| } | |
| if (bad) { | |
| // invalid character not found in MODES | |
| CString err("Invalid mode character: "); | |
| err.push_back(input_modes[ch]); | |
| throw MatcherError(err); | |
| } | |
| } | |
| } | |
| } | |
| if (IgnoreModes.count() == 0) { | |
| throw MatcherError("There are no modes for this ignore entry to act upon"); | |
| } | |
| } | |
| // return mode letters (mMaAnNcC) | |
| CString Matcher::Modes() const { | |
| CString s; | |
| for (int i = 0; i < NUM_MODES; i++) { | |
| if (IgnoreModes[i]) { | |
| s.push_back(MODES[i]); | |
| } | |
| } | |
| return s; | |
| } | |
| // return string representation of ignore modes bitmask for storage | |
| string Matcher::Bits() const { | |
| return IgnoreModes.to_string(); | |
| } | |
| bool Matcher::operator ==(const Matcher& other) const { | |
| return other.Data() == Data() && other.Type() == Type(); | |
| } | |
| bool Matcher::Match(CNick& nick, const CString& line, int mode) const { | |
| return IgnoreModes[mode]; | |
| } | |
| HostMatcher::HostMatcher(const CString& modes, const CString& mask) : Matcher(modes) { | |
| CNick host_tester(mask); | |
| CString host = host_tester.GetHostMask(); | |
| if (host.Equals(mask)) { | |
| Mask = mask; | |
| } else { | |
| throw MatcherError("HostMatcher: Malformed hostmask."); | |
| } | |
| } | |
| bool HostMatcher::Match(CNick& nick, const CString& message, int mode) const { | |
| return Matcher::Match(nick, message, mode) && nick.GetHostMask().WildCmp(Mask); | |
| } | |
| CString HostMatcher::String() const { | |
| return "Hostmask: " + Mask + " [" + Matcher::Modes() + "]"; | |
| } | |
| CString HostMatcher::Data() const { | |
| return Mask; | |
| } | |
| CString HostMatcher::Type() const { | |
| return "hostmask"; | |
| } | |
| RegexMatcher::RegexMatcher(const CString& modes, const CString& re_string) : Matcher(modes) { | |
| const char* re = re_string.c_str(); | |
| Pattern = re_string; | |
| int status = regcomp(&Regex, re, REG_EXTENDED|REG_NOSUB); | |
| if (status != 0) { | |
| size_t err_size = regerror(status, &Regex, NULL, 0); | |
| string error(err_size, '\0'); | |
| (void)regerror(status, &Regex, &error[0], err_size); | |
| throw MatcherError("RegexMatcher: Failed to compile regular expression /" + re_string + "/: " + error); | |
| } | |
| } | |
| bool RegexMatcher::Match(CNick& nick, const CString& message, int mode) const { | |
| return Matcher::Match(nick, message, mode) && regexec(&Regex, message.c_str(), 0, NULL, 0) == 0; | |
| } | |
| CString RegexMatcher::String() const { | |
| return "Regex: /" + Pattern + "/ [" + Matcher::Modes() + "]"; | |
| } | |
| CString RegexMatcher::Data() const { | |
| return Pattern; | |
| } | |
| CString RegexMatcher::Type() const { | |
| return CString("regex"); | |
| } | |
| RegexMatcher::~RegexMatcher() { | |
| regfree(&Regex); | |
| } | |
| /* | |
| * Now it's time for the actual module! | |
| */ | |
| ModIgnore::~ModIgnore() { | |
| cleanup(); | |
| } | |
| bool ModIgnore::OnLoad(const CString& args, CString& message) { | |
| for (MCString::iterator a = BeginNV(); a != EndNV(); ++a) { | |
| VCString parts; | |
| // modes|type | |
| size_t length = a->second.Split("|", parts); | |
| CString modes = parts[0]; | |
| CString type; | |
| CString data = a->first; | |
| // for compatibility with the previous version, a value without a pipe | |
| // in it will be just the modes, in which case assume default of | |
| // hostmask type ignore. | |
| if (length == 1) { | |
| type = "hostmask"; | |
| SetNV(data, modes + "|hostmask"); | |
| } else { | |
| type = parts[1]; | |
| } | |
| Matcher* m; | |
| try { | |
| if (type.Equals("hostmask")) { | |
| m = new HostMatcher(modes, data); // throws | |
| } else if (type.Equals("regex")) { | |
| m = new RegexMatcher(modes, data); // throws | |
| } else { | |
| message = "Invalid ignore type: '" + type + "'"; | |
| return false; | |
| } | |
| } catch (exception& err) { | |
| // TODO: return false? | |
| cerr << "ignore: Error: " << err.what() << endl; | |
| DelNV(a); | |
| continue; | |
| } | |
| IgnoreEntry e = { m }; | |
| IgnoreList.push_back(e); | |
| } | |
| size_t size = IgnoreList.size(); | |
| if (size > 0) { | |
| message = CString((int)size) + " ignore" + (size == 1 ? "" : "s") + " loaded."; | |
| } | |
| return true; | |
| } | |
| void ModIgnore::addIgnore(IgnoreEntry ignore) { | |
| int i = 0; | |
| for (vector<IgnoreEntry>::iterator a = IgnoreList.begin(); a != IgnoreList.end(); ++a) { | |
| if (*ignore.m == *a->m) { | |
| PutModule("Error: the ignore:"); | |
| PutModule(" " + a->m->String()); | |
| PutModule("already exists as entry #" + CString(i) + "."); | |
| PutModule("To add the entry again (e.g. with different modes), first delete the existing entry."); | |
| return; | |
| } | |
| i++; | |
| } | |
| IgnoreList.push_back(ignore); | |
| SetNV(ignore.m->Data(), ignore.m->Bits() + "|" + ignore.m->Type()); | |
| PutModule("Added " + ignore.m->String()); | |
| } | |
| // /msg *ignore AddHost [mode chars] <hostmask> | |
| void ModIgnore::CmdAddHostMatcher(const CString& line) { | |
| CString modes(MODES); | |
| CString mask; | |
| VCString tokens; | |
| int num = (int)line.QuoteSplit(tokens); | |
| switch (num) { | |
| case 0: | |
| // this should never happen | |
| case 1: | |
| { | |
| PutModule("Error: No hostmask specified."); | |
| return; | |
| } | |
| case 2: | |
| mask = tokens[1]; | |
| break; | |
| default: | |
| { | |
| modes = tokens[1]; | |
| mask = tokens[2]; | |
| } | |
| break; | |
| } | |
| try { | |
| Matcher* m = new HostMatcher(modes, mask); | |
| IgnoreEntry e = { m }; | |
| addIgnore(e); | |
| } catch (exception& err) { | |
| string msg("Error: "); | |
| msg += err.what(); | |
| PutModule(msg); | |
| PutModule("The entry will not be added to the ignore list."); | |
| } | |
| } | |
| // /msg *ignore AddPattern [mode chars] <hostmask> | |
| void ModIgnore::CmdAddRegexMatcher(const CString& line) { | |
| CString modes(MODES); | |
| CString re; | |
| VCString tokens; | |
| int num = (int)line.QuoteSplit(tokens); | |
| switch (num) { | |
| case 0: | |
| // this should never happen | |
| case 1: | |
| { | |
| PutModule("Error: No pattern specified."); | |
| return; | |
| } | |
| case 2: | |
| re = tokens[1]; | |
| break; | |
| default: | |
| { | |
| modes = tokens[1]; | |
| re = tokens[2]; | |
| } | |
| break; | |
| } | |
| try { | |
| Matcher* m = new RegexMatcher(modes, re); | |
| IgnoreEntry e = { m }; | |
| addIgnore(e); | |
| } catch (exception& err) { | |
| string msg("Error: "); | |
| msg += err.what(); | |
| PutModule(msg); | |
| PutModule("The entry will not be added to the ignore list."); | |
| } | |
| } | |
| void ModIgnore::CmdDelIgnore(const CString& line) { | |
| CString arg = line.Token(1); | |
| if (arg.empty()) { | |
| PutModule("Error: No index given."); | |
| return; | |
| } | |
| int index = arg.ToInt(); | |
| if (index > (int)IgnoreList.size()-1 || index < 0) { | |
| PutModule("Error: Invalid index."); | |
| return; | |
| } | |
| Matcher* m = IgnoreList[(size_t)index].m; | |
| // find the corresponding entry in the registry | |
| // we can't just use DelNV or FindNV directly because we have to see if the | |
| // type matches as well as the data | |
| for (MCString::iterator nv = BeginNV(); nv != EndNV(); ++nv) { | |
| VCString parts; | |
| // modes|type | |
| int size = (int)nv->second.Split("|", parts); | |
| CString type("hostmask"); | |
| if (size == 2) { | |
| type = parts[1]; | |
| } | |
| CString data = nv->first; | |
| if (type.Equals(m->Type()) && data.Equals(m->Data())) { | |
| DelNV(nv); | |
| CString s = m->String(); | |
| IgnoreList.erase(IgnoreList.begin() + index); | |
| PutModule("Deleted " + s); | |
| return; | |
| } | |
| } | |
| // this should never happen | |
| PutModule("Error: Couldn't find corresponding ignore in the ZNC module registry."); | |
| } | |
| void ModIgnore::CmdList(const CString& line) { | |
| if (IgnoreList.empty()) { | |
| PutModule("Ignore list is empty."); | |
| return; | |
| } | |
| CTable table; | |
| table.AddColumn("#"); | |
| table.AddColumn("Type"); | |
| table.AddColumn("Data"); | |
| table.AddColumn("Modes"); | |
| int i = 0; | |
| for (vector<IgnoreEntry>::iterator ignore = IgnoreList.begin(); ignore != IgnoreList.end(); ++ignore) { | |
| table.AddRow(); | |
| table.SetCell("#", CString(i)); | |
| table.SetCell("Type", ignore->m->Type()); | |
| table.SetCell("Data", ignore->m->Data()); | |
| table.SetCell("Modes", ignore->m->Modes()); | |
| i++; | |
| } | |
| PutModule(table); | |
| PutModule("(" + CString(IgnoreList.size()) + " entries)"); | |
| } | |
| void ModIgnore::CmdClear(const CString& line) { | |
| if (!ClearNV()) { | |
| PutModule("Error: Failed to clear ZNC module registry."); | |
| return; | |
| } | |
| int size = IgnoreList.size(); | |
| cleanup(); | |
| PutModule(CString(size) + " ignore" + (size == 1 ? "" : "s") + " erased."); | |
| } | |
| CModule::EModRet ModIgnore::check(CNick& nick, CString& message, int mode) { | |
| for (vector<IgnoreEntry>::iterator ignore = IgnoreList.begin(); ignore != IgnoreList.end(); ++ignore) { | |
| if (ignore->m->Match(nick, message, mode)) { | |
| return HALT; | |
| } | |
| } | |
| return CONTINUE; | |
| } | |
| CModule::EModRet ModIgnore::OnRawMessage(CMessage &message) { | |
| CString msgtext; | |
| IMode msgmode; | |
| CMessage::Type mtype = message.GetType(); | |
| if (mtype == CMessage::Type::Join) { | |
| msgmode = ModeJoin; | |
| } else if (mtype == CMessage::Type::Part) { | |
| msgmode = ModePart; | |
| } else if (mtype == CMessage::Type::Quit) { | |
| msgmode = ModeQuit; | |
| } else if (mtype == CMessage::Type::Nick) { | |
| msgmode = ModeNick; | |
| } else { | |
| return CONTINUE; | |
| } | |
| msgtext = message.ToString(message.IncludeAll); | |
| return check(message.GetNick(), msgtext, msgmode); | |
| } | |
| CModule::EModRet ModIgnore::OnChanMsg(CNick& nick, CChan& chan, CString& message) { | |
| return check(nick, message, ModeMsg); | |
| } | |
| CModule::EModRet ModIgnore::OnPrivMsg(CNick& nick, CString& message) { | |
| return check(nick, message, ModePrivMsg); | |
| } | |
| CModule::EModRet ModIgnore::OnChanAction(CNick& nick, CChan& chan, CString& message) { | |
| return check(nick, message, ModeAction); | |
| } | |
| CModule::EModRet ModIgnore::OnPrivAction(CNick& nick, CString& message) { | |
| return check(nick, message, ModePrivAction); | |
| } | |
| CModule::EModRet ModIgnore::OnChanNotice(CNick& nick, CChan& chan, CString& message) { | |
| return check(nick, message, ModeNotice); | |
| } | |
| CModule::EModRet ModIgnore::OnPrivNotice(CNick& nick, CString& message) { | |
| return check(nick, message, ModePrivNotice); | |
| } | |
| CModule::EModRet ModIgnore::OnChanCTCP(CNick& nick, CChan& chan, CString& message) { | |
| return check(nick, message, ModeCTCP); | |
| } | |
| CModule::EModRet ModIgnore::OnPrivCTCP(CNick& nick, CString& message) { | |
| return check(nick, message, ModePrivCTCP); | |
| } | |
| void ModIgnore::cleanup() { | |
| for (vector<IgnoreEntry>::iterator a = IgnoreList.begin(); a != IgnoreList.end(); ++a) { | |
| delete a->m; | |
| } | |
| IgnoreList.clear(); | |
| } | |
| MODULEDEFS(ModIgnore, "Ignore lines by hostmask or text pattern"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment