/*
* CSB Chat Filter
* Copyright (C) 2026 counter-strike-boost.com
*
* Cleans public chat: censors words from a blacklist (with word-boundary and
* simple leetspeak folding), blocks messages that contain IP addresses or URLs
* (anti-advertising), and gags repeat offenders for an escalating duration.
* Clean messages are re-emitted so normal chat still works.
*
* This program 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, either version 3 of the License, or (at your option) any later
* version. It 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 for details.
*/
#include
new const PLUGIN[] = "CSB Chat Filter"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
#define MAX_WORDS 16
new const g_szBadWords[][] =
{
"fuck", "shit", "bitch", "asshole", "bastard", "cunt", "nigger", "faggot"
}
new g_pEnabled, g_pAntiAd, g_pGagBase
new g_iOffenses[33]
new Float:g_fGagUntil[33]
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
g_pEnabled = register_cvar("csb_filter_enabled", "1")
g_pAntiAd = register_cvar("csb_filter_antiad", "1")
g_pGagBase = register_cvar("csb_filter_gagbase", "20")
register_clcmd("say", "cmdSay")
register_clcmd("say_team", "cmdSayTeam")
}
public client_putinserver(id)
{
g_iOffenses[id] = 0
g_fGagUntil[id] = 0.0
}
public cmdSay(id)
{
return handleChat(id, false)
}
public cmdSayTeam(id)
{
return handleChat(id, true)
}
handleChat(id, bool:team)
{
if (!get_pcvar_num(g_pEnabled) || !is_user_connected(id))
return PLUGIN_CONTINUE
new said[192]
read_args(said, charsmax(said))
remove_quotes(said)
trim(said)
if (!said[0])
return PLUGIN_HANDLED
/* still gagged? */
if (get_gametime() < g_fGagUntil[id])
{
new left = floatround(g_fGagUntil[id] - get_gametime(), floatround_ceil)
client_print(id, print_chat, "[CSB] You are gagged for %d more second(s).", left)
return PLUGIN_HANDLED
}
/* anti-advertising: block IPs / urls outright and escalate */
if (get_pcvar_num(g_pAntiAd) && looksLikeAd(said))
{
punish(id)
client_print(id, print_chat, "[CSB] Advertising is not allowed here.")
return PLUGIN_HANDLED
}
/* censor bad words in place */
new bool:censored = censorMessage(said)
if (censored)
punish(id)
new name[32]
get_user_name(id, name, charsmax(name))
reBroadcast(id, name, said, team)
return PLUGIN_HANDLED
}
bool:looksLikeAd(const msg[])
{
/* url schemes / common tlds */
if (containi(msg, "http") != -1 || containi(msg, "www.") != -1)
return true
if (containi(msg, ".com") != -1 || containi(msg, ".net") != -1
|| containi(msg, ".ro") != -1 || containi(msg, ".org") != -1)
return true
/* dotted-quad IP like 12.34.56.78 */
new dots = 0, digits = 0, run = 0
for (new i = 0; msg[i]; i++)
{
new c = msg[i]
if (c >= '0' && c <= '9')
{
digits++
run++
}
else if (c == '.')
{
if (run > 0)
dots++
run = 0
}
else
{
run = 0
}
}
if (dots >= 3 && digits >= 4)
return true
return false
}
/* fold a single character for leetspeak-insensitive matching */
foldChar(c)
{
switch (c)
{
case 'A', 'a', '@', '4': return 'a'
case 'E', 'e', '3': return 'e'
case 'I', 'i', '1', '!': return 'i'
case 'O', 'o', '0': return 'o'
case 'S', 's', '$', '5': return 's'
case 'T', 't', '7': return 't'
}
if (c >= 'A' && c <= 'Z')
return c + ('a' - 'A')
return c
}
bool:censorMessage(msg[])
{
new folded[192]
new len = strlen(msg)
if (len > charsmax(folded))
len = charsmax(folded)
for (new i = 0; i < len; i++)
folded[i] = foldChar(msg[i])
folded[len] = 0
new bool:hit = false
for (new w = 0; w < sizeof(g_szBadWords); w++)
{
new wlen = strlen(g_szBadWords[w])
new start = 0, pos
while ((pos = containi(folded[start], g_szBadWords[w])) != -1)
{
new at = start + pos
/* overwrite the matched span with asterisks */
for (new k = 0; k < wlen; k++)
msg[at + k] = '*'
hit = true
start = at + wlen
if (start >= len)
break
}
}
return hit
}
punish(id)
{
g_iOffenses[id]++
new base = get_pcvar_num(g_pGagBase)
if (base < 1)
base = 1
/* escalate: 1st strike warns, later strikes gag for base * (n-1) */
if (g_iOffenses[id] >= 2)
{
new secs = base * (g_iOffenses[id] - 1)
g_fGagUntil[id] = get_gametime() + float(secs)
client_print(id, print_chat, "[CSB] Watch your language. Gagged for %d second(s).", secs)
log_amx("[CSB Filter] gagged player id %d for %d s (offense #%d)", id, secs, g_iOffenses[id])
}
else
{
client_print(id, print_chat, "[CSB] Please keep the chat clean. This is a warning.")
}
}
reBroadcast(id, const name[], const msg[], bool:team)
{
new prefix[24]
if (team)
{
new t = get_user_team(id)
switch (t)
{
case 1: copy(prefix, charsmax(prefix), "(T) ")
case 2: copy(prefix, charsmax(prefix), "(CT) ")
default: copy(prefix, charsmax(prefix), "(Spec) ")
}
}
new out[224]
formatex(out, charsmax(out), "^x03%s%s^x01 : %s", prefix, name, msg)
new msgid = get_user_msgid("SayText")
new players[32], num
get_players(players, num, "ch")
for (new i = 0; i < num; i++)
{
message_begin(MSG_ONE, msgid, _, players[i])
write_byte(id)
write_string(out)
message_end()
}
}