mirror of
				https://github.com/jlu5/PyLink.git
				synced 2025-11-04 00:47:21 +01:00 
			
		
		
		
	antispam: implement text filters with optional PM spam checks
Also refactor the _punish code to account for events without a channel attached. Closes #359.
This commit is contained in:
		
							parent
							
								
									b0188dab92
								
							
						
					
					
						commit
						18bc1942e5
					
				@ -868,11 +868,21 @@ stats:
 | 
			
		||||
        # Sets the kick / kill message used when the text spamfilter is triggered.
 | 
			
		||||
        #reason: "Spam is prohibited"
 | 
			
		||||
 | 
			
		||||
    # Configures a (case-insensitive) list of bad strings to block in messages (PRIVMSG, NOTICE).
 | 
			
		||||
        # Determines whether PMs to PyLink clients should also be tracked to prevent spam.
 | 
			
		||||
        # Valid values include false (the boolean value), 'services', and 'all':
 | 
			
		||||
        # - If this is set to false, PMs are ignored by text filtering.
 | 
			
		||||
        # - If this is set to 'services', only PMs sent to services are checked for spam (this allows
 | 
			
		||||
        #   them to effectively act as PM spam traps)
 | 
			
		||||
        # - If this is set to 'all', all PyLink clients (including relay users) have incoming PMs
 | 
			
		||||
        #   checked for spam.
 | 
			
		||||
        # This defaults to false if not set.
 | 
			
		||||
        #watch_pms: false
 | 
			
		||||
 | 
			
		||||
    # Configures an (ASCII case-insensitive) list of bad strings to block in messages (PRIVMSG, NOTICE).
 | 
			
		||||
    # Globs are supported; use them to your advantage here.
 | 
			
		||||
    # You can also define server specific lists of bad strings by defining
 | 
			
		||||
    #   servers::<server name>::antispam_textfilters_messages
 | 
			
		||||
    # the contents of which will be merged into the global list specified below.
 | 
			
		||||
    #   servers::<server name>::antispam_textfilters_globs
 | 
			
		||||
    # the contents of which will be *merged* into the global list of bad strings specified below.
 | 
			
		||||
    #textfilter_globs:
 | 
			
		||||
        # - "*very bad don't say this*"
 | 
			
		||||
        # - "TWFkZSB5b3UgbG9vayE="
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,7 @@
 | 
			
		||||
# antispam.py: Basic services-side spamfilters for IRC
 | 
			
		||||
 | 
			
		||||
import ircmatch
 | 
			
		||||
 | 
			
		||||
from pylinkirc import utils, world, conf
 | 
			
		||||
from pylinkirc.log import log
 | 
			
		||||
 | 
			
		||||
@ -23,24 +25,25 @@ def _punish(irc, target, channel, punishment, reason):
 | 
			
		||||
 | 
			
		||||
    target_nick = irc.get_friendly_name(target)
 | 
			
		||||
 | 
			
		||||
    exempt_level = irc.get_service_option('antispam', 'exempt_level', DEFAULT_EXEMPT_OPTION).lower()
 | 
			
		||||
    c = irc.channels[channel]
 | 
			
		||||
    if channel:
 | 
			
		||||
        c = irc.channels[channel]
 | 
			
		||||
        exempt_level = irc.get_service_option('antispam', 'exempt_level', DEFAULT_EXEMPT_OPTION).lower()
 | 
			
		||||
 | 
			
		||||
    if exempt_level not in EXEMPT_OPTIONS:
 | 
			
		||||
        log.error('(%s) Antispam exempt %r is not a valid setting, '
 | 
			
		||||
                  'falling back to defaults; accepted settings include: %s',
 | 
			
		||||
                  irc.name, exempt_level, ', '.join(EXEMPT_OPTIONS))
 | 
			
		||||
        exempt_level = DEFAULT_EXEMPT_OPTION
 | 
			
		||||
        if exempt_level not in EXEMPT_OPTIONS:
 | 
			
		||||
            log.error('(%s) Antispam exempt %r is not a valid setting, '
 | 
			
		||||
                      'falling back to defaults; accepted settings include: %s',
 | 
			
		||||
                      irc.name, exempt_level, ', '.join(EXEMPT_OPTIONS))
 | 
			
		||||
            exempt_level = DEFAULT_EXEMPT_OPTION
 | 
			
		||||
 | 
			
		||||
    if exempt_level == 'voice' and c.is_voice_plus(target):
 | 
			
		||||
        log.debug("(%s) antispam: refusing to punish voiced and above %s/%s", irc.name, target, irc.get_friendly_name(target))
 | 
			
		||||
        return False
 | 
			
		||||
    elif exempt_level == 'halfop' and c.is_halfop_plus(target):
 | 
			
		||||
        log.debug("(%s) antispam: refusing to punish halfop and above %s/%s", irc.name, target, irc.get_friendly_name(target))
 | 
			
		||||
        return False
 | 
			
		||||
    elif exempt_level == 'op' and c.is_op_plus(target):
 | 
			
		||||
        log.debug("(%s) antispam: refusing to punish op and above %s/%s", irc.name, target, irc.get_friendly_name(target))
 | 
			
		||||
        return False
 | 
			
		||||
        if exempt_level == 'voice' and c.is_voice_plus(target):
 | 
			
		||||
            log.debug("(%s) antispam: refusing to punish voiced and above %s/%s", irc.name, target, target_nick)
 | 
			
		||||
            return False
 | 
			
		||||
        elif exempt_level == 'halfop' and c.is_halfop_plus(target):
 | 
			
		||||
            log.debug("(%s) antispam: refusing to punish halfop and above %s/%s", irc.name, target, target_nick)
 | 
			
		||||
            return False
 | 
			
		||||
        elif exempt_level == 'op' and c.is_op_plus(target):
 | 
			
		||||
            log.debug("(%s) antispam: refusing to punish op and above %s/%s", irc.name, target, target_nick)
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    my_uid = sbot.uids.get(irc.name)
 | 
			
		||||
    # XXX workaround for single-bot protocols like Clientbot
 | 
			
		||||
@ -81,7 +84,7 @@ def _punish(irc, target, channel, punishment, reason):
 | 
			
		||||
        elif action == 'kill':
 | 
			
		||||
            kill = True  # Delay kills so that the user data doesn't disappear.
 | 
			
		||||
        # XXX factorize these blocks
 | 
			
		||||
        elif action == 'kick':
 | 
			
		||||
        elif action == 'kick' and channel:
 | 
			
		||||
            try:
 | 
			
		||||
                _kick()
 | 
			
		||||
            except NotImplementedError:
 | 
			
		||||
@ -89,7 +92,7 @@ def _punish(irc, target, channel, punishment, reason):
 | 
			
		||||
                            "target was %s/%s", irc.name, target_nick, channel)
 | 
			
		||||
            else:
 | 
			
		||||
                successful_punishments += 1
 | 
			
		||||
        elif action == 'ban':
 | 
			
		||||
        elif action == 'ban' and channel:
 | 
			
		||||
            try:
 | 
			
		||||
                _ban()
 | 
			
		||||
            except (ValueError, NotImplementedError):
 | 
			
		||||
@ -97,7 +100,7 @@ def _punish(irc, target, channel, punishment, reason):
 | 
			
		||||
                            "target was %s/%s", irc.name, target_nick, channel)
 | 
			
		||||
            else:
 | 
			
		||||
                successful_punishments += 1
 | 
			
		||||
        elif action == 'quiet':
 | 
			
		||||
        elif action == 'quiet' and channel:
 | 
			
		||||
            try:
 | 
			
		||||
                _quiet()
 | 
			
		||||
            except (ValueError, NotImplementedError):
 | 
			
		||||
@ -182,13 +185,95 @@ def handle_masshighlight(irc, source, command, args):
 | 
			
		||||
            punishment = mhl_settings.get('punishment', MASSHIGHLIGHT_DEFAULTS['punishment']).lower()
 | 
			
		||||
            reason = mhl_settings.get('reason', MASSHIGHLIGHT_DEFAULTS['reason'])
 | 
			
		||||
 | 
			
		||||
            log.debug('(%s) antispam: calling _punish on %s/%s', irc.name,
 | 
			
		||||
                      source, irc.get_friendly_name(source))
 | 
			
		||||
            log.info("(%s) antispam: punishing %s => %s for mass highlight spam",
 | 
			
		||||
                     irc.name,
 | 
			
		||||
                     irc.get_friendly_name(source),
 | 
			
		||||
                     channel)
 | 
			
		||||
            punished = _punish(irc, source, channel, punishment, reason)
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    log.debug('(%s) antispam: got %s/%s nicks on message to %r', irc.name, len(nicks_caught), min_nicks, channel)
 | 
			
		||||
    log.debug('(%s) antispam.masshighlight: got %s/%s nicks on message to %r', irc.name,
 | 
			
		||||
              len(nicks_caught), min_nicks, channel)
 | 
			
		||||
    return not punished  # Filter this message from relay, etc. if it triggered protection
 | 
			
		||||
 | 
			
		||||
utils.add_hook(handle_masshighlight, 'PRIVMSG', priority=1000)
 | 
			
		||||
utils.add_hook(handle_masshighlight, 'NOTICE', priority=1000)
 | 
			
		||||
 | 
			
		||||
TEXTFILTER_DEFAULTS = {
 | 
			
		||||
    'reason': "Spam is prohibited",
 | 
			
		||||
    'punishment': 'kick+ban',
 | 
			
		||||
    'watch_pms': 'false',
 | 
			
		||||
    'enabled': False
 | 
			
		||||
}
 | 
			
		||||
def handle_textfilter(irc, source, command, args):
 | 
			
		||||
    """Antispam text filter handler."""
 | 
			
		||||
    target = args['target']
 | 
			
		||||
    text = args['text']
 | 
			
		||||
    txf_settings = irc.get_service_option('antispam', 'textfilter',
 | 
			
		||||
                                          TEXTFILTER_DEFAULTS)
 | 
			
		||||
 | 
			
		||||
    if not txf_settings.get('enabled', False):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    my_uid = sbot.uids.get(irc.name)
 | 
			
		||||
 | 
			
		||||
    # XXX workaround for single-bot protocols like Clientbot
 | 
			
		||||
    if irc.pseudoclient and not irc.has_cap('can-spawn-clients'):
 | 
			
		||||
        my_uid = irc.pseudoclient.uid
 | 
			
		||||
 | 
			
		||||
    if (not irc.connected.is_set()) or (not my_uid):
 | 
			
		||||
        # Break if the network isn't ready.
 | 
			
		||||
        log.debug("(%s) antispam.textfilters: skipping processing; network isn't ready", irc.name)
 | 
			
		||||
        return
 | 
			
		||||
    elif irc.is_internal_client(source):
 | 
			
		||||
        # Ignore messages from our own clients.
 | 
			
		||||
        log.debug("(%s) antispam.textfilters: skipping processing message from internal client %s", irc.name, source)
 | 
			
		||||
        return
 | 
			
		||||
    elif source not in irc.users:
 | 
			
		||||
        log.debug("(%s) antispam.textfilters: ignoring message from non-user %s", irc.name, source)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if irc.is_channel(target):
 | 
			
		||||
        channel_or_none = target
 | 
			
		||||
        if target not in irc.channels or my_uid not in irc.channels[target].users:
 | 
			
		||||
            # We're not monitoring this channel.
 | 
			
		||||
            log.debug("(%s) antispam.textfilters: skipping processing message from channel %r we're not in", irc.name, target)
 | 
			
		||||
            return
 | 
			
		||||
    else:
 | 
			
		||||
        channel_or_none = None
 | 
			
		||||
        watch_pms = txf_settings.get('watch_pms', TEXTFILTER_DEFAULTS['watch_pms'])
 | 
			
		||||
 | 
			
		||||
        if watch_pms == 'services':
 | 
			
		||||
            if not irc.get_service_bot(target):
 | 
			
		||||
                log.debug("(%s) antispam.textfilters: skipping processing; %r is not a service bot (watch_pms='services')", irc.name, target)
 | 
			
		||||
                return
 | 
			
		||||
        elif watch_pms == 'all':
 | 
			
		||||
            log.debug("(%s) antispam.textfilters: checking all PMs (watch_pms='all')", irc.name)
 | 
			
		||||
            pass
 | 
			
		||||
        else:
 | 
			
		||||
            # Not a channel.
 | 
			
		||||
            log.debug("(%s) antispam.textfilters: skipping processing; %r is not a channel and watch_pms is disabled", irc.name, target)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
    # Merge together global and local textfilter lists.
 | 
			
		||||
    txf_globs = set(conf.conf.get('antispam', {}).get('textfilter_globs', [])) | \
 | 
			
		||||
                set(irc.serverdata.get('antispam_textfilter_globs', []))
 | 
			
		||||
 | 
			
		||||
    punishment = txf_settings.get('punishment', TEXTFILTER_DEFAULTS['punishment']).lower()
 | 
			
		||||
    reason = txf_settings.get('reason', TEXTFILTER_DEFAULTS['reason'])
 | 
			
		||||
 | 
			
		||||
    punished = False
 | 
			
		||||
    for filterglob in txf_globs:
 | 
			
		||||
        if ircmatch.match(1, filterglob, text):
 | 
			
		||||
            log.info("(%s) antispam: punishing %s => %s for text filter %r",
 | 
			
		||||
                     irc.name,
 | 
			
		||||
                     irc.get_friendly_name(source),
 | 
			
		||||
                     irc.get_friendly_name(target),
 | 
			
		||||
                     filterglob)
 | 
			
		||||
            punished = _punish(irc, source, channel_or_none, punishment, reason)
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    return not punished  # Filter this message from relay, etc. if it triggered protection
 | 
			
		||||
 | 
			
		||||
utils.add_hook(handle_textfilter, 'PRIVMSG', priority=999)
 | 
			
		||||
utils.add_hook(handle_textfilter, 'NOTICE', priority=999)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user