diff --git a/warns.py b/warns.py index bbb24d7..f64c01c 100644 --- a/warns.py +++ b/warns.py @@ -1,352 +1,355 @@ # -*- coding: utf-8 -*- from database import * import tools reIdentIP = re.compile(r'!([0-9a-fA-F]{8})@.*/.*') def loadbl(): "Load the blacklist" global blacklist terms = [t[0] == '/' and t[-1] == '/' and t.strip('/') or '\\b' + re.escape(t) for t in chanList['global'].get('black', ())] try: blacklist = re.compile('|'.join(terms)) except: blacklist = None bot.msg('#wikimedia-opbot', 'Error while compiling blacklist') if not 'recentActions' in globals(): recentActions = defaultdict(lambda: deque(maxlen=40)) chanWarns = defaultdict(list) warnDelay = {} warned = deque(maxlen=50) loadbl() chanDisabled = {} tracked = {} recentKRB = deque(maxlen=10) def mkping(channel, flag=None, inchan=['#wikimedia-opbot', '#wikimedia-ops']): "Return a set of channel ops nicks" if not channel in chanList or channel == '#wikimedia-overflow': return set() if isinstance(inchan, basestring): inchan = [inchan] accounts = {op for op in chanList[channel]['access'] if 'o' in chanList[channel]['access'][op]['flags'].lower()} - \ {'wmopbot', 'Sigyn', 'eir'} if flag and 'ping' in chanList['global']: flag = set(flag) accounts &= {a for a, args in chanList['global']['ping'].iteritems() if flag & set(args.get('flags', ''))} nicks = {nick for chan in inchan for nick in chanNicks.get(chan, ())} return {nick for nick in nicks if nickUser.get(nick, {}).get('account') in accounts} def warn(target, action, channel, ping=False, warnChan=None, delay=3): ping = ping and mkping(channel, ping) or set() warned.append((target, action, channel, time.time())) channel = type(channel) == list and channel or channel and [channel] or [] for chan in ['#wikimedia-opbot'] + (warnChan and (isinstance(warnChan, basestring) and [warnChan] or warnChan) or []): if chan in chanDisabled and ('all' in chanDisabled[chan] or not set(channel) - chanDisabled[chan]): print 'Warn disabled in %s: %s %s %s' % (chan, target, action, mklist(channel)) continue chanWarns[chan].append((target, action, channel, ping, time.time())) if delay: if not target in warnDelay: warnDelay[target] = reactor.callLater(delay, sendWarns, target) else: sendWarns(target) def sendWarns(target): if target in warnDelay: if warnDelay[target].active(): warnDelay[target].cancel() del warnDelay[target] now = time.time() # warn -> ('target', 'action', 'channel', set(ping), float(time)) # select all warns of the same target targetWarns = [(chan, w) for chan in chanWarns for w in chanWarns[chan] if w[0] == target and w[4] + 10 > now] if not targetWarns: return uniqueWarns = [] # merge warns to warn in the same channel for chan in {c for c, w in targetWarns}: cwarns = [w for c, w in targetWarns if c == chan] # if same target has warns with different actions in different channels, split the warn per action if len({w[1] for w in cwarns}) > 1 and len({repr(w[2]) for w in cwarns}) > 1: for action in {w[1] for w in cwarns}: uniqueWarns.append((chan, [w for w in cwarns if w[1] == action])) else: uniqueWarns.append((chan, cwarns)) chansUnique = [] for chan, warns in uniqueWarns: # if it is only one warn, add other warns with different target but same action and channel if len(warns) == 1: warns += [w for w in chanWarns[chan] if w[0] != target and w[1] == warns[0][1] and w[2] == warns[0][2] and w[4] + 10 > now] for w in warns: chanWarns[chan].remove(w) temp = [] nicks = mklist([w[0] for w in warns if not (w[0] in temp or temp.append(w[0]))]) actions = mklist([w[1] for w in warns if not (w[1] in temp or temp.append(w[1]))]) if actions.count(' in') > 1: actions = actions.replace(' in', '', 1) channels = mklist({c for w in warns for c in w[2]}) ping = set() if '\x0303' in actions else {n for w in warns for n in w[3]} # merge identic warns to send to diferent channels in one command (/PRIVMSG #chan1,#chan2 :message) # in order to avoid disconnection per Excess Flood for i, items in enumerate(chansUnique): if [nicks, actions, channels] == items[1:4]: chansUnique[i][0] += ',' + chan chansUnique[i][4] |= ping break else: chansUnique.append([chan, nicks, actions, channels, ping]) for chan, nicks, actions, channels, ping in chansUnique: ping = ' '.join(sorted(ping)) message = '%s %s %s%s' % (nicks, actions, channels, ping and '\x0315 ping %s\x03' % ping or '') if re.search(r'was (kickbanned|removed|banned)', actions) and nicks in nickUser: inchans = [c for c in chanNicks if nicks in chanNicks[c]] if inchans: message += ', they are also in ' + mklist(inchans) bot.msg(chan, message) def iswarned(target, action, channel, sec=60): now = time.time() for t, a, c, tm in warned: if (t, a, c) == (target, action, channel) and tm + sec > now: return True return False def join(user, channel): "Called when an user join a channel" identhost = '%(ident)s@%(host)s' % user ra = recentActions[identhost] ra.appendleft(('join', channel, time.time())) towarn = [] ping = '' identIP = reIdentIP.search(user.full) if tools.isinlist(user, 'track'): entries = mklist(entry for entry in chanList['global']['track'] if (user.account == entry[3:] if entry[0:3] == '$a:' else '!' in entry and maskre(entry).match(user.full))) towarn.append('tracked (%s)' % entries) ping += 't' elif user.host.startswith(wikimediaCloaks) or tools.isinlist(user, 'exempt'): return elif identhost in tracked and tracked[identhost][1] + 3600 > time.time(): towarn.append('recently ' + tracked[identhost][0]) ping += 't' elif ('ip' in user or identIP) and user.score < 10: ip = user.ip or '%d.%d.%d.%d' % tuple(int(identIP.group(1)[i:i + 2], 16) for i in (0, 2, 4, 6)) blocks = tools.wikiBlock(ip) if blocks: towarn.append('IP blocked in ' + blocks) ping += 'w' wikis = ' '.join(set(re.findall('global|\w+wiki', blocks))) tools.logAction(user=user, channel=channel, action='ip-block', target=wikis) target = user.full globalBans = [ban for ban, mask in tools.chanBans['#wikimedia-bans'] if mask.match(target)] if user.account: accban = '$a:' + irclower(user.account) globalBans += [ban for ban in chanList['#wikimedia-bans']['ban'] if ban[0] == '$' and irclower(ban) == accban] if globalBans: towarn.append('global banned (%s)' % ', '.join(globalBans)) ping += 'g' ra.appendleft(('gbanned join', channel, time.time())) action = '(%s) \x0307joined\x03' % mklist(towarn) if towarn and ('tracked' in towarn or not iswarned(user.nick, action, channel, 86400)): warn(user.nick, action, channel, ping) def part(user, channel, reason=''): "Called when an user leaves a channel" identhost = '%(ident)s@%(host)s' % user ra = recentActions[identhost] match = re.match(r'requested by (\S+)', reason) if match: ra.appendleft(('removed', channel, time.time())) recentKRB.appendleft(('removed', user.full, channel, match.group(1), time.time())) tools.logAction(user=nickUser[match.group(1)], channel=channel, action='remove', target=user) for act, banned, chan, by, tm in recentKRB: if not (act == 'banned' and tm + 15 > time.time() and (channel == chan or channel in jBans.get(chan, ())) and maskre(banned).match(user.full)): continue if by == match.group(1): warn(user.nick, 'was \x0303banned and removed\x03 by %s from' % match.group(1), channel, warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops', delay=1) else: warn(user.nick, 'was \x0303banned\x03 by %s and \x0303removed\x03 by %s from' % (by, match.group(1)), channel, warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops', delay=1) kickbanned(target, channel) break else: def removeWarn(): for act, chan, tm in ra: if act == 'kickban' and tm + 15 > time.time(): break else: warn(user.nick, 'was \x0303removed\x03 by %s from' % match.group(1), channel, warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops', delay=0) reactor.callLater(15, removeWarn) return ra.appendleft(('part', channel, time.time())) if tools.isinlist(user, 'track'): warn(user.nick, '\x0303parted\x03', channel, delay=1) return timeCheck = time.time() - 5 last5sec = [act for act, chan, tm in ra if chan == channel and tm > timeCheck] if 'join' in last5sec and 'msg' in last5sec: warn(user.nick, '\x0307spammed (join-say-part)\x03', channel, 's', warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops') def quit(user, channels, reason): "Called when an user quit" identhost = '%(ident)s@%(host)s' % user recentActions[identhost].appendleft(('quit', reason, time.time())) if reason.startswith('Killed ('): by = reason.split('(')[1].strip() if by.endswith('.freenode.net'): return warn(user.nick, 'was \x0303killed\x03 by %s in' % by, channels, warnChan='#wikimedia-ops', delay=1) elif reason == 'K-Lined': warn(user.nick, 'was \x0303K-Lined\x03 in', channels, warnChan='#wikimedia-ops', delay=1) elif tools.isinlist(user, 'track'): warn(user.nick, '\x0303quit\x03 (%s) from' % reason, channels, delay=1) def msg(user, channel, msg): "Called when an user send a message to a channel" # ignore users with affiliated cloak if user.get('cloak', 'unaffiliated') != 'unaffiliated' or tools.isinlist(user, 'exempt') \ or chanNicks[channel].get(user.nick): return now = time.time() repChar = re.search(r'(\S)\1{8,}', msg) tooConsonants = re.search(r'[bcdfghjklmnpqrstvwxz]{8,}', re.sub(r'https?://\S+', '', msg)) blmatch = blacklist and blacklist.search(msg) m = blmatch or repChar or tooConsonants if m: action = blmatch and 'said a \x0307blacklisted term\x03' or repChar and '\x0307repeated character\x03' \ or tooConsonants and '\x0307many consective consonants\x03' start = re.search(r'\S+ ?$', msg[0:m.start()]) end = re.search(r'^ ?\S+', msg[m.end():]) match = (start and start.group(0) or '') + '\x1f' + m.group(0)[0:100] + '\x1f' + (end and end.group(0) or '') if user.get('score', 0) < 2: warn(user.nick, '%s (%s) in' % (action, match), channel, 'b', warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops', delay=1) tools.logAction(user=user, channel=channel, action='blacklist', target=m.group(0), note=msg) else: warn('%s (score %d)' % (user.nick, user.get('score', 0)), '%s (%s) in' % (action, match[0:100]), channel, False, delay=1) return ra = recentActions['%(ident)s@%(host)s' % user] ra.appendleft(('msg', channel, now)) if len([(a, t) for a, c, t in ra if c == channel and a == 'msg' and t + 20 > now]) > 10 and \ not iswarned(user.nick, '\x0307flooding\x03', channel): warn(user.nick, '\x0307flooding\x03', channel, 's', warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops') + elif len(set(msg.split()) & chanNicks[channel].viewkeys()) > 5: + warn(user.nick, '\x0307ping spam\x03', channel, 's', + warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops') def renamed(user, oldNick): "Called when an user nick is changed" recentActions['%(ident)s@%(host)s' % user].appendleft(('renamed', None, time.time())) def notice(user, channel, msg): "Called when an user send a notice to a channel" if user.get('cloak', 'unaffiliated') != 'unaffiliated' or tools.isinlist(user, 'exempt'): return recentActions['%(ident)s@%(host)s' % user].appendleft(('notice', None, time.time())) if user.account in ('wmopbot', 'ChanServ', 'Sigyn', 'eir'): return if not iswarned(user.nick, '\x0307sent notice\x03 to', channel): if channel.startswith('#wikimedia-opbot') or 'AntiSpamMeta' in chanNicks[channel]: warn(user.nick, '\x0307sent notice\x03', channel, 's') else: warn(user.nick, '\x0307sent notice\x03', channel, 's', warnChan='#wikimedia-ops') def banned(user, channel, target): "Called when I see a ban action" if target == '*!*@*': return recentKRB.appendleft(('banned', target, channel, user.nick, time.time())) if target.startswith('$a:'): account = target[3:] banUsers = [u for u in nickUser.itervalues() if u.account == account] elif target[0] != '$' and '!' in target: mask = maskre(target) if not mask: return banUsers = [u for u in nickUser.itervalues() if mask.match(u.full)] else: return banChans = [channel] + jBans.get(channel, []) for target in banUsers: ra = recentActions['%(ident)s@%(host)s' % target] ra.appendleft(('banned', channel, time.time())) for act, kicked, chan, by, tm in recentKRB: if not (act in ('kicked', 'removed') and chan in banChans and target.full.endswith(kicked.split('!')[1]) and tm + 15 > time.time()): continue elif act == 'kicked' and by == user.nick: warn(target.nick, 'was \x0303kickbanned\x03 by %s from' % user.nick, chan, warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops', delay=1) elif act == 'removed' and by == user.nick: warn(target.nick, 'was \x0303removed and banned\x03 by %s from' % user.nick, chan, warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops', delay=1) else: warn(target.nick, 'was \x0303%s\x03 by %s and \x0303banned\x03 by %s from' % (act, by, user.nick), chan, warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops', delay=1) kickbanned(target, channel) def kicked(user, channel, target, reason): "Called when I see a kick action" ra = recentActions['%(ident)s@%(host)s' % target] ra.appendleft(('kicked', channel, time.time())) recentKRB.appendleft(('kicked', target.full, channel, user.nick, time.time())) for act, banned, chan, by, tm in recentKRB: if not (act == 'banned' and tm + 15 > time.time() and (channel == chan or channel in jBans.get(chan, ())) and maskre(banned).match(target.full)): continue if by == user.nick: warn(target.nick, 'was \x0303kickbanned\x03 by %s from' % user.nick, channel, warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops', delay=1) else: warn(target.nick, 'was \x0303banned\x03 by %s and \x0303kicked\x03 by %s from' % (by, user.nick), channel, warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops', delay=1) kickbanned(target, channel) break else: def kickWarn(): for act, chan, tm in ra: if act == 'kickban' and tm + 15 > time.time(): break else: warn(target.nick, 'was \x0303kicked\x03 by %s from' % user.nick, channel, warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops', delay=0) reactor.callLater(15, kickWarn) def kickbanned(target, channel): "Called after kickban warning" identhost = '%(ident)s@%(host)s' % target ra = recentActions[identhost] ra.appendleft(('kickban', channel, time.time())) tracked[identhost] = ('kickbanned from ' + channel, time.time()) ftarget = target.full globalBans = [ban for ban, mask in tools.chanBans['#wikimedia-bans'] if mask.match(ftarget)] sugBan = 'ip' in target and ['*!*@' + (target.host.startswith('gateway/') and '*' or '') + target.ip] or [] if target.account: accban = '$a:' + target.account globalBans += [ban for ban in chanList['#wikimedia-bans']['ban'] if ban == accban] sugBan += [accban] sugBan = [b for b in sugBan if not b in globalBans] if len({chan for act, chan, tm in ra if act == 'kickban' and not chan.startswith('#wikimedia-opbot')}) > 1 \ and sugBan and not iswarned(identhost, 'suggest gban', '#wikimedia-ops'): message = 'Two recent kickbans for %s, I suggest to global ban %s \x0315ping %s' % (target.nick, mklist(sugBan), mklist(sorted(mkping('#wikimedia-bans', False, inchan='#wikimedia-ops')))) warned.append((identhost, 'suggest gban', '#wikimedia-ops', time.time())) print '[%s] WARN: #wikimedia-ops %s' % (time.strftime('%Y-%m-%d %H:%M:%S'), message) reactor.callLater(2, bot.msg, '#wikimedia-ops', message) def topic(user, channel, new): if user.host == 'services.': return if not 't' in chanList[channel]['config']['cs/irc'].get('modes', '') and not 'cloak' in user \ and not tools.isinlist(user, 'exempt'): warn(user.nick, '(%s) \x0307changed the topic\x03' % ('unreg' in user and 'unregistered' or 'unaffiliated'), channel, 's', warnChan=not channel.startswith('#wikimedia-opbot') and '#wikimedia-ops')