Browse Source

m_block_masshighlight: Merged improvements by k4be -- conf vars allow_accessmode/percent/show_opers_origmsg + handling, chmode +H to exempt a channel from checks and don't count duplicate nicks on the same line

Wazakindjes 1 year ago
parent
commit
3e128766f3
2 changed files with 208 additions and 33 deletions
  1. 10 1
      README.md
  2. 198 32
      u4/m_block_masshighlight.c

+ 10 - 1
README.md

@@ -106,6 +106,9 @@ The hostmask is matched against `user@realhost` only. The timestring shit like `
 This shit can halp you prevent highlight spam on your entire network. =] It keeps track of a user's messages on a per-channel basis and checks if they highlight one person too many times or too many different persons at once. Opers and U:Lines are exempt (as per usual), but also those with list modes `+a` and `+q`. ;3 When someone hits the threshold, opers with the snomask `EYES` will get a server notice through the module (enable that shit with `/mode <nick> +s +e` etc ;]).
 
 __Updated shit:__
+* Don't count duplicate nicks on the same line as separate highlights (also added/proposed by [k4be](https://forums.unrealircd.org/memberlist.php?mode=viewprofile&u=39884))
+* Added chanmode `+H` to exempt a channel from __all__ mass highlight checks, which mite b useful for quiz channels (also added/proposed by [k4be](https://forums.unrealircd.org/memberlist.php?mode=viewprofile&u=39884))
+* Added conf directives (and message handling for) `allow_accessmode`, `percent`, `show_opers_origmsg` (added/proposed by [k4be](https://forums.unrealircd.org/memberlist.php?mode=viewprofile&u=39884))
 * Added conf directive `allow_authed` (see bel0w bruh)
 * Added action `viruschan`
 * Added conf directive `multiline` imo tbh fambi (see below for m0ar inf0)
@@ -123,9 +126,12 @@ _The module doesn't necessarily require any configuration, it uses the following
     	banident 1;
     	multiline 0;
     	allow_authed 0;
+    	//allow_accessmode o; // k4be
+    	percent 1; // k4be
+    	show_opers_origmsg 1; // k4be
     };
 
-* __maxnicks__:  Maximum amount of highlights (going over this number results in `action` setting in)
+* __maxnicks__:  Maximum amount of highlighted nicks (going over this number results in `action` setting in) -- __works in conjunction with `percent`__
 * __delimiters__: List of characters to split a sentence by (don't forget the surrounding quotes lol) -- any char not in the default list may prevent highlights anyways
 * __action__: Action to take, must be one of: `drop` (drop silently [for the offender]), `notice` (drop, but __do__ show notice to them), `gline`, `zline`, `shun`, `tempshun`, `kill`, `viruschan`
 * __duration__: How long to `gline`, `zline` or `shun` for, is a "timestring" like `7d`, `1h5m20s`, etc
@@ -134,6 +140,9 @@ _The module doesn't necessarily require any configuration, it uses the following
 * __banident__: When set to `1` it will ban `ident@iphost`, otherwise `*@iphost` (useful for shared ZNCs etc)
 * __multiline__: When set to `1` it will keep counting highlights until it encounters a line __without__ one
 * __allow_authed__: When set to `1` it will let logged-in users bypass this shit
+* __allow_accessmode__: Must be __one__ of `vhoaq` (or omitted entirely for no exemptions [the default]), exempts everyone with at minimum the specified mode from highlight checks (e.g. `a` includes people with `+q`)
+* __percent__: Threshold for the amount of characters belonging to highlights, not counting `delimiters` (e.g. `hi nick` would be 67%) -- __works in conjunction with `maxnicks`__
+* __show_opers_origmsg__: Display the message that was dropped to opers with the `SNO_EYES` snomask set
 
 If you omit a directive which is required for a certain `action`, you'll get a warning and it will proceed to use the default. It should be pretty clear what directives are required in what cases imo tbh famalam. ;];]
 

+ 198 - 32
u4/m_block_masshighlight.c

@@ -4,6 +4,10 @@
 // Config block
 #define MYCONF "block_masshighlight"
 
+// Channel mode for exempting from highlight checks
+#define IsNocheckHighlights(chptr) (chptr->mode.extmode & EXTCMODE_NOCHECK_MASSHIGHLIGHT)
+#define CHMODE_CHAR 'H'
+
 // Dem macros yo
 #define IsMDErr(x, y, z) \
 	do { \
@@ -14,7 +18,8 @@
 	} while(0)
 
 // Quality fowod declarations
-int is_chanowner_prot(aClient *sptr, aChannel *chptr);
+int is_accessmode_exempt(aClient *sptr, aChannel *chptr);
+int extcmode_requireowner(aClient *sptr, aChannel *chptr, char mode, char *para, int checkt, int what);
 void doXLine(char flag, aClient *sptr);
 int masshighlight_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
 int masshighlight_configposttest(int *errs);
@@ -26,6 +31,7 @@ char *masshighlight_hook_prechanmsg(aClient *sptr, aChannel *chptr, char *text,
 extern MODVAR int spamf_ugly_vchanoverride; // For viruschan shit =]
 static ModuleInfo *massHLMI = NULL; // Store ModuleInfo so we can use it to check for errors in MOD_LOAD
 ModDataInfo *massHLMDI; // To store some shit with the channel ;]
+Cmode_t EXTCMODE_NOCHECK_MASSHIGHLIGHT; // For storing the exemption chanmode =]
 
 struct {
 	unsigned short int maxnicks; // Maxnicks, unsigned cuz can't be negative anyways lol
@@ -37,6 +43,9 @@ struct {
 	unsigned short int banident; // Whether to ban ident@host or simply *@host
 	unsigned short int multiline; // Check over multiple lines or just the one ;]
 	unsigned short int allow_authed; // Allow identified users to bypass this shit
+	unsigned int allow_accessmode; // Lowest channel access mode privilege to bypass the limit
+	unsigned short int percent; // How many characters in a message recognised as a nickname is enough for the message to be rejected
+	unsigned short int show_opers_origmsg; // Display the suspicious message to operators
 
 	// These are just for setting to 0 or 1 to see if we got em config directives ;]
 	unsigned short int got_maxnicks;
@@ -48,12 +57,15 @@ struct {
 	unsigned short int got_banident;
 	unsigned short int got_multiline;
 	unsigned short int got_allow_authed;
+	unsigned short int got_allow_accessmode;
+	unsigned short int got_percent;
+	unsigned short int got_show_opers_origmsg;
 } muhcfg;
 
 // Dat dere module header
 ModuleHeader MOD_HEADER(m_block_masshighlight) = {
 	"m_block_masshighlight", // Module name
-	"$Id: v1.03 2017/09/13 Gottem$", // Version
+	"$Id: v1.04 2017/11/19 Gottem/k4be$", // Version
 	"Prevent mass highlights network-wide", // Description
 	"3.2-b8-1", // Modversion, not sure wat do
 	NULL
@@ -81,6 +93,14 @@ MOD_INIT(m_block_masshighlight) {
 	massHLMDI = ModDataAdd(modinfo->handle, mreq);
 	IsMDErr(massHLMDI, m_block_masshighlight, modinfo);
 
+	// Also request +H channel mode fam
+	CmodeInfo req;
+	memset(&req, 0, sizeof(req));
+	req.paracount = 0; // No args required ;]
+	req.flag = CHMODE_CHAR;
+	req.is_ok = extcmode_requireowner; // Need owner privs to set em imo tbh
+	CmodeAdd(modinfo->handle, req, &EXTCMODE_NOCHECK_MASSHIGHLIGHT);
+
 	// Register hewks m8
 	HookAddPChar(modinfo->handle, HOOKTYPE_PRE_CHANMSG, 0, masshighlight_hook_prechanmsg);
 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, masshighlight_configrun);
@@ -109,19 +129,19 @@ MOD_UNLOAD(m_block_masshighlight) {
 	return MOD_SUCCESS; // We good
 }
 
-int is_chanowner_prot(aClient *sptr, aChannel *chptr) {
+// Client exempted through allow_accessmode?
+int is_accessmode_exempt(aClient *sptr, aChannel *chptr) {
 	Membership *lp; // For checkin' em list access level =]
 
 	if(IsServer(sptr) || IsMe(sptr)) // Allow servers always lel
 		return 1;
 
+	if(!muhcfg.allow_accessmode) // Don't even bother ;]
+		return 0;
+
 	if(chptr) { // Sanity cheqq
 		if((lp = find_membership_link(sptr->user->channel, chptr))) {
-			#ifdef PREFIX_AQ
-				if(lp->flags & (CHFL_CHANOWNER|CHFL_CHANPROT))
-			#else
-				if(lp->flags & CHFL_CHANOP)
-			#endif
+			if(lp->flags & muhcfg.allow_accessmode)
 				return 1;
 		}
 	}
@@ -129,6 +149,15 @@ int is_chanowner_prot(aClient *sptr, aChannel *chptr) {
 	return 0; // No valid channel/membership or doesn't have enough axx lol
 }
 
+// Testing for owner status on channel
+int extcmode_requireowner(aClient *sptr, aChannel *chptr, char mode, char *para, int checkt, int what) {
+	if(IsPerson(sptr) && is_chanowner(sptr, chptr))
+		return EX_ALLOW;
+	if(checkt == EXCHK_ACCESS_ERR)
+		sendto_one(sptr, err_str(ERR_CHANOWNPRIVNEEDED), me.name, sptr->name, chptr->chname);
+	return EX_DENY;
+}
+
 // Not using place_host_ban() cuz I need more control over the mask to ban ;];;];]
 void doXLine(char flag, aClient *sptr) {
 	// Double check for sptr existing, cuz inb4segfault
@@ -162,6 +191,7 @@ void doXLine(char flag, aClient *sptr) {
 int masshighlight_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) {
 	int errors = 0; // Error count
 	int i; // Iterat0r
+	int tmp; // f0 checking integer values
 	ConfigEntry *cep; // To store the current variable/value pair etc
 
 	// Since we'll add a top-level block to unrealircd.conf, need to filter on CONFIG_MAIN lmao
@@ -190,6 +220,7 @@ int masshighlight_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *err
 			if(!cep->ce_vardata || !strlen(cep->ce_vardata)) {
 				config_error("%s:%i: %s::%s must be non-empty fam", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
 				errors++; // Increment err0r count fam
+				continue;
 			}
 
 			// Checkem valid actions
@@ -198,6 +229,9 @@ int masshighlight_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *err
 				config_error("%s:%i: %s::%s must be one of: drop, notice, gline, zline, shun, tempshun, kill, viruschan", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
 				errors++;
 			}
+
+			if(!errors)
+				muhcfg.action = cep->ce_vardata[0]; // We need this value to be set in posttest
 			continue;
 		}
 
@@ -245,7 +279,7 @@ int masshighlight_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *err
 				}
 			}
 			if(!errors) {
-				int tmp = atoi(cep->ce_vardata);
+				tmp = atoi(cep->ce_vardata);
 				if(tmp <= 0 || tmp > 512) {
 					config_error("%s:%i: %s::%s must be an integer between 1 and 512 m8", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
 					errors++; // Increment err0r count fam
@@ -290,6 +324,69 @@ int masshighlight_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *err
 			continue;
 		}
 
+		if(!strcmp(cep->ce_varname, "allow_accessmode")) {
+			muhcfg.got_allow_accessmode = 1;
+			if(!cep->ce_vardata || !strlen(cep->ce_vardata)) {
+				config_error("%s:%i: %s::%s must be either non-empty or not specified at all", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+				errors++; // Increment err0r count fam
+				continue;
+			}
+
+			if(strlen(cep->ce_vardata) > 1) {
+				config_error("%s:%i: %s::%s must be exactly one character (mode) in length", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+				errors++; // Increment err0r count fam
+				continue;
+			}
+
+			if(strcmp(cep->ce_vardata, "v") && strcmp(cep->ce_vardata, "h") && strcmp(cep->ce_vardata, "o")
+		#ifdef PREFIX_AQ
+				&& strcmp(cep->ce_vardata, "a") && strcmp(cep->ce_vardata, "q")
+		#endif
+			) {
+				config_error("%s:%i: %s::%s must be one of: v, h, o"
+		#ifdef PREFIX_AQ
+				", a, q"
+		#endif
+				, cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+				errors++;
+			}
+			continue;
+		}
+
+		if(!strcmp(cep->ce_varname, "percent")) {
+			muhcfg.got_percent = 1;
+			// Should be an integer yo
+			if(!cep->ce_vardata) {
+				config_error("%s:%i: %s::%s must be an integer between 1 and 100 m8", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+				errors++; // Increment err0r count fam
+				continue;
+			}
+			for(i = 0; cep->ce_vardata[i]; i++) {
+				if(!isdigit(cep->ce_vardata[i])) {
+					config_error("%s:%i: %s::%s must be an integer between 1 and 100 m8", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+					errors++; // Increment err0r count fam
+					break;
+				}
+			}
+			if(!errors) {
+				tmp = atoi(cep->ce_vardata);
+				if(tmp <= 0 || tmp > 100) {
+					config_error("%s:%i: %s::%s must be an integer between 1 and 100 m8", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+					errors++; // Increment err0r count fam
+				}
+			}
+			continue;
+		}
+
+		if(!strcmp(cep->ce_varname, "show_opers_origmsg")) {
+			muhcfg.got_show_opers_origmsg = 1;
+			if(!cep->ce_vardata || (strcmp(cep->ce_vardata, "0") && strcmp(cep->ce_vardata, "1"))) {
+				config_error("%s:%i: %s::%s must be either 0 or 1 fam", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname);
+				errors++; // Increment err0r count fam
+			}
+			continue;
+		}
+
 		// Anything else is unknown to us =]
 		config_warn("%s:%i: unknown item %s::%s", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname); // So display just a warning
 	}
@@ -334,6 +431,15 @@ int masshighlight_configposttest(int *errs) {
 	if(!muhcfg.got_allow_authed)
 		muhcfg.allow_authed = 0; // Default to n0 fam
 
+	if(!muhcfg.got_allow_accessmode)
+		muhcfg.allow_accessmode = 0; // None ;]
+
+	if(!muhcfg.got_percent)
+		muhcfg.percent = 1; // 1%, max sensitivity
+
+	if(!muhcfg.got_show_opers_origmsg)
+		muhcfg.show_opers_origmsg = 1; // Default to showing em
+
 	*errs = errors;
 	return errors ? -1 : 1;
 }
@@ -360,11 +466,6 @@ int masshighlight_configrun(ConfigFile *cf, ConfigEntry *ce, int type) {
 		if(!cep->ce_varname)
 			continue; // Next iteration imo tbh
 
-		if(!strcmp(cep->ce_varname, "action")) {
-			muhcfg.action = cep->ce_vardata[0];
-			continue;
-		}
-
 		if(!strcmp(cep->ce_varname, "delimiters")) {
 			if(muhcfg.delimiters) // Let's free this first lol (should never happen but let's just in case xd)
 				free(muhcfg.delimiters);
@@ -408,6 +509,43 @@ int masshighlight_configrun(ConfigFile *cf, ConfigEntry *ce, int type) {
 			muhcfg.allow_authed = atoi(cep->ce_vardata);
 			continue;
 		}
+
+		if(!strcmp(cep->ce_varname, "allow_accessmode")) {
+			muhcfg.allow_accessmode = 0;
+			switch(cep->ce_vardata[0]) {
+				case 'v':
+					muhcfg.allow_accessmode |= CHFL_VOICE;
+					break;
+				case 'h':
+					muhcfg.allow_accessmode |= CHFL_HALFOP;
+					break;
+				case 'o':
+					muhcfg.allow_accessmode |= CHFL_CHANOP;
+					break;
+		#ifdef PREFIX_AQ
+				case 'a':
+					muhcfg.allow_accessmode |= CHFL_CHANPROT;
+					break;
+				case 'q':
+					muhcfg.allow_accessmode |= CHFL_CHANOWNER;
+					break;
+		#endif
+				default:
+					break;
+			}
+			continue;
+		}
+
+		if(!strcmp(cep->ce_varname, "percent")) {
+			muhcfg.percent = atoi(cep->ce_vardata);
+			continue;
+		}
+
+		if(!strcmp(cep->ce_varname, "show_opers_origmsg")) {
+			muhcfg.show_opers_origmsg = atoi(cep->ce_vardata);
+			continue;
+		}
+
 	}
 
 	return 1; // We good
@@ -425,50 +563,75 @@ char *masshighlight_hook_prechanmsg(aClient *sptr, aChannel *chptr, char *text,
 	** text: The message body obv fambi
 	** notice: Was it a NOTICE or simply PRIVMSG?
 	*/
-	int hl; // Current highlight count yo
-	char *p = NULL; // For tokenising that shit
+	int hl_cur; // Current highlight count yo
+	char *p; // For tokenising that shit
 	char *werd; // Store current token etc
-	char *cleantext = NULL; // Let's not modify char *text =]
+	char *cleantext; // Let's not modify char *text =]
 	aClient *acptr; // Temporarily store a mentioned nick to get the membership link
 	Membership *lp; // For actually storing the link
 	MembershipL *lp2; // Required for moddata call apparently
-	int blockem = 0; // Need to block the message?
-	int clearem = 1; // For clearing the counter
+	int blockem; // Need to block the message?
+	int clearem; // For clearing the counter
 	int ret; // For checking the JOIN return status for viruschan action
 	char joinbuf[CHANNELLEN + 4]; // Also for viruschan, need to make room for "0,#viruschan" etc ;];]
 	char *vcparv[3]; // Arguments for viruschan JOIN
-	int bypassem = (muhcfg.allow_authed && IsPerson(sptr) && IsLoggedIn(sptr)); // If config var is tr00 and user es logged in
-
-	// Some sanity + privilege checks ;];] (+a, +q, U:Lines and opers are exempt from this shit)
-	if(text && IsPerson(sptr) && MyClient(sptr) && !bypassem && !is_chanowner_prot(sptr, chptr) && !IsULine(sptr) && !IsOper(sptr)) {
+	int bypass_nsauth; // If config var allow_authed is tr00 and user es logged in
+	char gotnicks[1024]; // For storing nicks that were mentioned =]
+	int hl_new; // Store highlight count for further processing
+	size_t werdlen; // Word length ;]
+	size_t hl_nickslen; // Amount of chars that are part of highlights
+	size_t msglen; // Full message length (both of these are required for calculating percent etc), excludes delimiters
+
+	// Initialise some shit lol
+	bypass_nsauth = (muhcfg.allow_authed && IsPerson(sptr) && IsLoggedIn(sptr));
+	blockem = 0;
+	clearem = 1;
+	p = NULL;
+	cleantext = NULL;
+	memset(gotnicks, '\0', sizeof(gotnicks));
+	hl_new = 0;
+	hl_nickslen = 0;
+	msglen = 0;
+
+	if(IsNocheckHighlights(chptr))
+		return text; // if channelmode is set, allow without further checking
+
+	// Some sanity + privilege checks ;];] (allow_accessmode, U:Lines and opers are exempt from this shit)
+	if(text && IsPerson(sptr) && MyClient(sptr) && !bypass_nsauth && !is_accessmode_exempt(sptr, chptr) && !IsULine(sptr) && !IsOper(sptr)) {
 		if(!(lp = find_membership_link(sptr->user->channel, chptr))) // Not even a member tho lol
 			return text; // Process normally cuz can't do shit w/ moddata =]
 
 		lp2 = (MembershipL *)lp; // Cast that shit
-		hl = moddata_membership(lp2, massHLMDI).i; // Get current count
+		hl_cur = moddata_membership(lp2, massHLMDI).i; // Get current count
 
 		// In case someone tries some funny business =]
 		if(!(cleantext = (char *)StripColors(text)) || !(cleantext = (char *)StripControlCodes(cleantext)))
 			return text;
 
 		for(werd = strtoken(&p, cleantext, muhcfg.delimiters); werd; werd = strtoken(&p, NULL, muhcfg.delimiters)) { // Split that shit
+			werdlen = strlen(werd);
+			msglen += werdlen; // We do not count ze delimiters, otherwise the percents would get strangely low
 			if((acptr = find_person(werd, NULL)) && (find_membership_link(acptr->user->channel, chptr))) { // User mentioned another user in this channel
-				hl += 1; // So increment counter
-				clearem = 0; // And flip the clear flag to n0
-
-				if(hl > muhcfg.maxnicks) { // No need to tokenise more if we're already over threshold
-					blockem = 1; // Flip flag to blockin' dat shit
-					break;
+				if(!(strstr(gotnicks, acptr->id))) { // Do not count the same nickname appended multiple times to a single message
+					ircsnprintf(gotnicks, sizeof(gotnicks), "%s%s,", gotnicks, acptr->id);
+					hl_new++; // Got another highlight this round
+					hl_nickslen += werdlen; // Also add the nick's length
 				}
 			}
 		}
 
+		if(100 * hl_nickslen / msglen > muhcfg.percent) { // Check if we exceed the max allowed percentage
+			hl_cur += hl_new; // Set moddata counter to include the ones found this round
+			clearem = 0; // And don't clear moddata
+			if(hl_cur > muhcfg.maxnicks) // Check if we also exceed max allowed nicks
+				blockem = 1; // Flip flag to blockin' dat shit
+		}
+
 		if(!muhcfg.multiline && !clearem) // If single line mode and found a nick
 			clearem = 1; // Need to clear that shit always lol
 
-		moddata_membership(lp2, massHLMDI).i = (clearem ? 0 : hl); // Actually set the counter =]
+		moddata_membership(lp2, massHLMDI).i = (clearem ? 0 : hl_cur); // Actually set the counter =]
 		if(blockem) { // Need to bl0ck?
-			text = NULL; // NULL makes Unreal drop the message entirely ;3
 			switch(muhcfg.action) {
 				case 'd': // Drop silently
 					if(muhcfg.snotice)
@@ -533,6 +696,9 @@ char *masshighlight_hook_prechanmsg(aClient *sptr, aChannel *chptr, char *text,
 				default:
 					break;
 			}
+			if(muhcfg.show_opers_origmsg)
+				sendto_snomask_global(SNO_EYES, "*** [block_masshighlight] The message was: %s", text);
+			text = NULL; // NULL makes Unreal drop the message entirely afterwards ;3
 		}
 	}