pissircd/src/modules/chanmodes/censor.c
Bram Matthys 9691a6d819
Create TextAnalysis framework (hook), this counts the unicode block
switches like antimixedutf8 did, and counts the number of characters
used per unicode block. Potentially more can be added later, this is
flexible and modules can add stuff (..well not yet.. the struct is
missing some members..).

Use it from antimixedutf8 so that it now uses the new code, which is
similar to what I made and then reverted in July 2023:
3e2f668f10
..except that it now calculated in src/modules/utf8functions.c.
But yeah, this needs more testing and possibly (default) score
adjustments to deal with false positives !! And a warning in release notes :D

Put the text analysis in ClientContext member textanalysis,
so typically accessed through clictx->textanalysis.
Note that this struct can (and often is) NULL, for example if it is
a remote client, if it is not a PRIVMSG/NOTICE (will improve later)
or if the utf8functions module is not loaded (to keep things optional).

BREAKING CHANGE is that ClientContext is now passed in the
HOOKTYPE_CAN_SEND_TO_CHANNEL and HOOKTYPE_CAN_SEND_TO_USER hooks.

So HOOKTYPE_CAN_SEND_TO_USER prototype changed from:
int hooktype_can_send_to_user(Client *client, Client *target, const char **text, const char **errmsg, SendType sendtype);
To:
int hooktype_can_send_to_user(Client *client, Client *target, const char **text, const char **errmsg, SendType sendtype, ClientContext *clictx);

And HOOKTYPE_CAN_SEND_TO_CHANNEL prototype changes from:
int hooktype_can_send_to_channel(Client *client, Channel *channel, Membership *member, const char **text, const char **errmsg, SendType sendtype);
To:
int hooktype_can_send_to_channel(Client *client, Channel *channel, Membership *member, const char **text, const char **errmsg, SendType sendtype, ClientContext *clictx);

A side-affect of this change for antimixedutf8 purposes is that,
while the analysis is only done once per line, the 'actions' are
performed for each target, so the action will run 4 times for
"PRIVMSG a,b,c,d :text" although that may not be important in
practice. Just mentioning.
2025-03-23 11:44:24 +01:00

338 lines
7.7 KiB
C

/*
* Channel Mode +G
* (C) Copyright 2005-current Bram Matthys and The UnrealIRCd team.
*/
#include "unrealircd.h"
ModuleHeader MOD_HEADER
= {
"chanmodes/censor",
"4.2",
"Channel Mode +G",
"UnrealIRCd Team",
"unrealircd-6",
};
Cmode_t EXTMODE_CENSOR = 0L;
#define IsCensored(x) ((x)->mode.mode & EXTMODE_CENSOR)
int censor_can_send_to_channel(Client *client, Channel *channel, Membership *lp, const char **msg, const char **errmsg, SendType sendtype, ClientContext *clictx);
const char *censor_pre_local_part(Client *client, Channel *channel, const char *text);
const char *censor_pre_local_quit(Client *client, const char *text);
int censor_stats_badwords_channel(Client *client, const char *para);
int censor_config_test(ConfigFile *, ConfigEntry *, int, int *);
int censor_config_run(ConfigFile *, ConfigEntry *, int);
ModuleInfo *ModInfo = NULL;
ConfigItem_badword *conf_badword_channel = NULL;
MOD_TEST()
{
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, censor_config_test);
return MOD_SUCCESS;
}
MOD_INIT()
{
CmodeInfo req;
ModInfo = modinfo;
MARK_AS_OFFICIAL_MODULE(modinfo);
memset(&req, 0, sizeof(req));
req.paracount = 0;
req.is_ok = extcmode_default_requirechop;
req.letter = 'G';
CmodeAdd(modinfo->handle, req, &EXTMODE_CENSOR);
HookAdd(modinfo->handle, HOOKTYPE_CAN_SEND_TO_CHANNEL, 0, censor_can_send_to_channel);
HookAddConstString(modinfo->handle, HOOKTYPE_PRE_LOCAL_PART, 0, censor_pre_local_part);
HookAddConstString(modinfo->handle, HOOKTYPE_PRE_LOCAL_QUIT, 0, censor_pre_local_quit);
HookAdd(modinfo->handle, HOOKTYPE_STATS, 0, censor_stats_badwords_channel);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, censor_config_run);
return MOD_SUCCESS;
}
MOD_LOAD()
{
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
ConfigItem_badword *badword, *next;
for (badword = conf_badword_channel; badword; badword = next)
{
next = badword->next;
DelListItem(badword, conf_badword_channel);
badword_config_free(badword);
}
return MOD_SUCCESS;
}
int censor_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
ConfigEntry *cep;
char has_word = 0, has_replace = 0, has_action = 0, action = 'r';
if (type != CONFIG_MAIN)
return 0;
if (!ce || !ce->name || strcmp(ce->name, "badword"))
return 0; /* not interested */
if (!ce->value)
{
config_error("%s:%i: badword without type",
ce->file->filename, ce->line_number);
*errs = 1;
return -1;
}
else if (strcmp(ce->value, "channel") &&
strcmp(ce->value, "quit") && strcmp(ce->value, "all")) {
/* config_error("%s:%i: badword with unknown type",
ce->file->filename, ce->line_number); -- can't do that.. */
return 0; /* unhandled */
}
if (!strcmp(ce->value, "quit"))
{
config_error("%s:%i: badword quit has been removed. We just use the bad words from "
"badword channel { } instead.",
ce->file->filename, ce->line_number);
return 0; /* pretend unhandled.. ok not just pretend.. ;) */
}
for (cep = ce->items; cep; cep = cep->next)
{
if (config_is_blankorempty(cep, "badword"))
{
errors++;
continue;
}
if (!strcmp(cep->name, "word"))
{
const char *errbuf;
if (has_word)
{
config_warn_duplicate(cep->file->filename,
cep->line_number, "badword::word");
continue;
}
has_word = 1;
if ((errbuf = badword_config_check_regex(cep->value,1,1)))
{
config_error("%s:%i: badword::%s contains an invalid regex: %s",
cep->file->filename,
cep->line_number,
cep->name, errbuf);
errors++;
}
}
else if (!strcmp(cep->name, "replace"))
{
if (has_replace)
{
config_warn_duplicate(cep->file->filename,
cep->line_number, "badword::replace");
continue;
}
has_replace = 1;
}
else if (!strcmp(cep->name, "action"))
{
if (has_action)
{
config_warn_duplicate(cep->file->filename,
cep->line_number, "badword::action");
continue;
}
has_action = 1;
if (!strcmp(cep->value, "replace"))
action = 'r';
else if (!strcmp(cep->value, "block"))
action = 'b';
else
{
config_error("%s:%d: Unknown badword::action '%s'",
cep->file->filename, cep->line_number,
cep->value);
errors++;
}
}
else
{
config_error_unknown(cep->file->filename, cep->line_number,
"badword", cep->name);
errors++;
}
}
if (!has_word)
{
config_error_missing(ce->file->filename, ce->line_number,
"badword::word");
errors++;
}
if (has_action)
{
if (has_replace && action == 'b')
{
config_error("%s:%i: badword::action is block but badword::replace exists",
ce->file->filename, ce->line_number);
errors++;
}
}
*errs = errors;
return errors ? -1 : 1;
}
int censor_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
ConfigEntry *cep, *word = NULL;
ConfigItem_badword *ca;
if (type != CONFIG_MAIN)
return 0;
if (!ce || !ce->name || strcmp(ce->name, "badword"))
return 0; /* not interested */
if (strcmp(ce->value, "channel") && strcmp(ce->value, "all"))
return 0; /* not for us */
ca = safe_alloc(sizeof(ConfigItem_badword));
ca->action = BADWORD_REPLACE;
for (cep = ce->items; cep; cep = cep->next)
{
if (!strcmp(cep->name, "action"))
{
if (!strcmp(cep->value, "block"))
{
ca->action = BADWORD_BLOCK;
}
}
else if (!strcmp(cep->name, "replace"))
{
safe_strdup(ca->replace, cep->value);
} else
if (!strcmp(cep->name, "word"))
{
word = cep;
}
}
badword_config_process(ca, word->value);
if (!strcmp(ce->value, "channel"))
AddListItem(ca, conf_badword_channel);
else if (!strcmp(ce->value, "all"))
{
AddListItem(ca, conf_badword_channel);
return 0; /* pretend we didn't see it, so other modules can handle 'all' as well */
}
return 1;
}
const char *stripbadwords_channel(const char *str, int *blocked)
{
return stripbadwords(str, conf_badword_channel, blocked);
}
int censor_can_send_to_channel(Client *client, Channel *channel, Membership *lp, const char **msg, const char **errmsg, SendType sendtype, ClientContext *clictx)
{
int blocked;
Hook *h;
int i;
if (!IsCensored(channel))
return HOOK_CONTINUE;
for (h = Hooks[HOOKTYPE_CAN_BYPASS_CHANNEL_MESSAGE_RESTRICTION]; h; h = h->next)
{
i = (*(h->func.intfunc))(client, channel, BYPASS_CHANMSG_CENSOR);
if (i == HOOK_ALLOW)
return HOOK_CONTINUE; /* bypass censor restriction */
if (i != HOOK_CONTINUE)
break;
}
*msg = stripbadwords_channel(*msg, &blocked);
if (blocked)
{
*errmsg = "Swearing is not permitted in this channel";
return HOOK_DENY;
}
return HOOK_CONTINUE;
}
const char *censor_pre_local_part(Client *client, Channel *channel, const char *text)
{
int blocked;
if (!text)
return NULL;
if (!IsCensored(channel))
return text;
text = stripbadwords_channel(text, &blocked);
return blocked ? NULL : text;
}
/** Is any channel where the user is in +G? */
static int IsAnyChannelCensored(Client *client)
{
Membership *lp;
for (lp = client->user->channel; lp; lp = lp->next)
if (IsCensored(lp->channel))
return 1;
return 0;
}
const char *censor_pre_local_quit(Client *client, const char *text)
{
int blocked = 0;
if (!text)
return NULL;
if (IsAnyChannelCensored(client))
text = stripbadwords_channel(text, &blocked);
return blocked ? NULL : text;
}
int censor_stats_badwords_channel(Client *client, const char *para)
{
ConfigItem_badword *words;
if (!para || !(!strcmp(para, "b") || !strcasecmp(para, "badword")))
return 0;
for (words = conf_badword_channel; words; words = words->next)
{
sendtxtnumeric(client, "c %c %s%s%s %s", words->type & BADW_TYPE_REGEX ? 'R' : 'F',
(words->type & BADW_TYPE_FAST_L) ? "*" : "", words->word,
(words->type & BADW_TYPE_FAST_R) ? "*" : "",
words->action == BADWORD_REPLACE ? (words->replace ? words->replace : "<censored>") : "");
}
return 1;
}