# # BigBrotherBot(B3) (www.bigbrotherbot.net) # Copyright (C) 2005 Michael "ThorN" Thornton # # 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 2 of the License, or # (at your option) any later version. # # 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, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # CHANGELOG # 2012/07/03 - 3.1 - Courgette # Fixes a bug wich prevented regular expression "\sd[i!1*]ck\s" to match for word "d!ck" # 2012/07/03 - 3.0.1 - Courgette # Gives meaningful log messages when loading the config file # 2011/12/26 - 3.0 - Courgette # Refactor and make the checks on raw text before checks on cleaned text. Add tests # 2/12/2011 - 2.2.2 - Bravo17 # Fix for reason keyword not working # 1/16/2010 - 2.2.1 - xlr8or # Plugin can now be disabled with !disable censor # 1/16/2010 - 2.2.0 - xlr8or # Added ignore_length as an optional configurable option # Started debugging the badname checker # 8/13/2005 - 2.0.0 - ThorN # Converted to use XML config # Allow custom penalties for words and names # 7/23/2005 - 1.1.0 - ThorN # Added data column to penalties table # Put censored message/name in the warning data __author__ = 'ThorN, xlr8or, Bravo17, Courgette' __version__ = '3.1' import b3, re, traceback, sys, threading import b3.events import b3.plugin from b3.config import XmlConfigParser from b3 import functions class PenaltyData: def __init__(self, **kwargs): for k, v in kwargs.iteritems(): setattr(self, k, v) type = None reason = None keyword = None duration = 0 def __repr__(self): return """Penalty(type=%r, reason=%r, keyword=%r, duration=%r)""" % (self.type, self.reason, self.keyword, self.duration) def __str__(self): data = {"type": self.type, "reason": self.reason, "reasonkeyword": self.keyword, "duration": self.duration} return "<penalty " + ' '.join(['%s="%s"' % (k, v) for k, v in data.items() if v]) + " />" class CensorData: def __init__(self, **kwargs): for k, v in kwargs.iteritems(): setattr(self, k, v) name = None penalty = None regexp = None def __repr__(self): return """CensorData(name=%r, penalty=%r, regexp=%r)""" % (self.name, self.penalty, self.regexp) #-------------------------------------------------------------------------------------------------- class CensorPlugin(b3.plugin.Plugin): _adminPlugin = None _reClean = re.compile(r'[^0-9a-z ]+', re.I) _defaultBadWordPenalty = PenaltyData(type="warning", keyword="cuss") _defaultBadNamePenalty = PenaltyData(type="warning", keyword="badname") _maxLevel = 0 _ignoreLength = 3 def onStartup(self): self._adminPlugin = self.console.getPlugin('admin') if not self._adminPlugin: return False self.registerEvent(b3.events.EVT_CLIENT_SAY) self.registerEvent(b3.events.EVT_CLIENT_TEAM_SAY) self.registerEvent(b3.events.EVT_CLIENT_NAME_CHANGE) self.registerEvent(b3.events.EVT_CLIENT_AUTH) def onLoadConfig(self): assert isinstance(self.config, XmlConfigParser) try: self._maxLevel = self.config.getint('settings', 'max_level') except Exception, err: self._maxLevel = 0 self.warning(err) self.warning("using default value %s for settings:max_level" % self._maxLevel) try: self._ignoreLength = self.config.getint('settings', 'ignore_length') except Exception, err: self._ignoreLength = 3 self.warning(err) self.warning("using default value %s for settings:ignore_length" % self._ignoreLength) default_badwords_penalty_nodes = self.config.get('badwords/penalty') if len(default_badwords_penalty_nodes): penalty = default_badwords_penalty_nodes[0] self._defaultBadWordPenalty = PenaltyData(type = penalty.get('type'), reason = penalty.get('reason'), keyword = penalty.get('reasonkeyword'), duration = functions.time2minutes(penalty.get('duration'))) else: self.warning("""no default badwords penalty found in config. Using default : %s""" % self._defaultBadNamePenalty) default_badnames_penalty_nodes = self.config.get('badnames/penalty') if len(default_badnames_penalty_nodes): penalty = default_badnames_penalty_nodes[0] self._defaultBadNamePenalty = PenaltyData(type = penalty.get('type'), reason = penalty.get('reason'), keyword = penalty.get('reasonkeyword'), duration = functions.time2minutes(penalty.get('duration'))) else: self.warning("""no default badnames penalty found in config. Using default : %s""" % self._defaultBadNamePenalty) # load bad words into memory self._badWords = [] for e in self.config.get('badwords/badword'): penalty_node = e.find('penalty') word_node = e.find('word') regexp_node = e.find('regexp') self._add_bad_word(rulename=e.get('name'), penalty=penalty_node, word=word_node.text if word_node is not None else None, regexp=regexp_node.text if regexp_node is not None else None) # load bad names into memory self._badNames = [] for e in self.config.get('badnames/badname'): penalty_node = e.find('penalty') word_node = e.find('word') regexp_node = e.find('regexp') self._add_bad_name(rulename=e.get('name'), penalty=penalty_node, word=word_node.text if word_node is not None else None, regexp=regexp_node.text if regexp_node is not None else None) def _add_bad_word(self, rulename, penalty=None, word=None, regexp=None): if word is regexp is None: self.warning("badword rule [%s] has no word and no regular expression to search for" % rulename) elif word is not None and regexp is not None: self.warning("badword rule [%s] cannot have both a word and regular expression to search for" % rulename) elif regexp is not None: # has a regular expression self._badWords.append(self._getCensorData(rulename, regexp.strip(), penalty, self._defaultBadWordPenalty)) self.debug("badword rule '%s' loaded" % rulename) elif word is not None: # has a plain word self._badWords.append(self._getCensorData(rulename, '\\s' + word.strip() + '\\s', penalty, self._defaultBadWordPenalty)) self.debug("badword rule '%s' loaded" % rulename) def _add_bad_name(self, rulename, penalty=None, word=None, regexp=None): if word is regexp is None: self.warning("badname rule [%s] has no word and no regular expression to search for" % rulename) elif word is not None and regexp is not None: self.warning("badname rule [%s] cannot have both a word and regular expression to search for" % rulename) elif regexp is not None: # has a regular expression self._badNames.append(self._getCensorData(rulename, regexp.strip(), penalty, self._defaultBadNamePenalty)) self.debug("badname rule '%s' loaded" % rulename) elif word is not None: # has a plain word self._badNames.append(self._getCensorData(rulename, '\\s' + word.strip() + '\\s', penalty, self._defaultBadNamePenalty)) self.debug("badname rule '%s' loaded" % rulename) def _getCensorData(self, name, regexp, penalty, defaultPenalty): try: regexp = re.compile(regexp, re.I) except re.error, e: self.error('Invalid regular expression: %s - %s' % (name, regexp)) raise if penalty is not None: pd = PenaltyData(type = penalty.get('type'), reason = penalty.get('reason'), keyword = penalty.get('reasonkeyword'), duration = functions.time2minutes(penalty.get('duration'))) else: pd = defaultPenalty return CensorData(name=name, penalty=pd, regexp=regexp) def onEvent(self, event): try: if not self.isEnabled(): return elif not event.client: return elif event.client.cid is None: return elif event.client.maxLevel > self._maxLevel: return elif not event.client.connected: return if event.type == b3.events.EVT_CLIENT_AUTH or event.type == b3.events.EVT_CLIENT_NAME_CHANGE: self.checkBadName(event.client) elif len(event.data) > self._ignoreLength: if event.type == b3.events.EVT_CLIENT_SAY or \ event.type == b3.events.EVT_CLIENT_TEAM_SAY: self.checkBadWord(event.data, event.client) except b3.events.VetoEvent: raise except Exception, msg: self.error('Censor plugin error: %s - %s', msg, traceback.extract_tb(sys.exc_info()[2])) def penalizeClient(self, penalty, client, data=''): """\ This is the default penalisation for using bad language in say and teamsay """ #self.debug("%s"%((penalty.type, penalty.reason, penalty.keyword, penalty.duration),)) # fix for reason keyword not working if penalty.keyword is None: penalty.keyword = penalty.reason self._adminPlugin.penalizeClient(penalty.type, client, penalty.reason, penalty.keyword, penalty.duration, None, data) def penalizeClientBadname(self, penalty, client, data=''): """\ This is the penalisation for bad names """ #self.debug("%s"%((penalty.type, penalty.reason, penalty.keyword, penalty.duration),)) self._adminPlugin.penalizeClient(penalty.type, client, penalty.reason, penalty.keyword, penalty.duration, None, data) def checkBadName(self, client): if not client.connected: self.debug('Client not connected?') return cleaned_name = ' ' + self.clean(client.exactName) + ' ' self.info("Checking '%s'=>'%s' for badname" % (client.exactName, cleaned_name)) was_penalized = False for w in self._badNames: if w.regexp.search(client.exactName): self.debug("badname rule [%s] matches '%s'" % (w.name, client.exactName)) self.penalizeClientBadname(w.penalty, client, '%s (rule %s)' % (client.exactName, w.name)) was_penalized = True break if w.regexp.search(cleaned_name): self.debug("badname rule [%s] matches cleaned name '%s' for player '%s'" % (w.name, cleaned_name, client.exactName)) self.penalizeClientBadname(w.penalty, client, '%s (rule %s)' % (client.exactName, w.name)) was_penalized = True break if was_penalized: # check again in 1 minute t = threading.Timer(60, self.checkBadName, (client,)) t.start() return def checkBadWord(self, text, client): cleaned = ' ' + self.clean(text) + ' ' text = ' ' + text + ' ' self.debug("cleaned text: [%s]" % cleaned) for w in self._badWords: if w.regexp.search(text): self.debug("badword rule [%s] matches '%s'" % (w.name, text)) self.penalizeClient(w.penalty, client, text) raise b3.events.VetoEvent if w.regexp.search(cleaned): self.debug("badword rule [%s] matches cleaned text '%s'" % (w.name, cleaned)) self.penalizeClient(w.penalty, client, '%s => %s' % (text, cleaned)) raise b3.events.VetoEvent def clean(self, data): return re.sub(self._reClean, ' ', self.console.stripColors(data.lower()))