#!/usr/bin/env python3 # How to read a line. def deirc(nick, line): action = False if len(line) <= 3: return action, nick, line if line[0] == '\u200b': if line[1] == '<': try: close = line.index('>') assert(line[close+1] == ' ') assert(not any(map(lambda x: x.isspace(), line[2:close]))) nick = line[2:close] line = line[close+2:] except: pass elif line[1:3] == '* ': try: close = line[3:].index(' ') + 3 assert(not any(map(lambda x: x.isspace(), line[3:close]))) nick = line[3:close] line = line[close+1:] action = True except: pass elif line[:8] == '\x01ACTION ' and line[-1] == '\x01': action = True line = line[8:-1] # Redact the nologs. if line.startswith('[nolog]') or line.startswith('nolog:'): line = '[REDACTED]' return action, nick, line # Set flags and funcs. from sys import argv if len(argv) != 4: print('Usage: [Sspy] in out') exit(1) _, flags, fin, fout = argv methods = [] # Read the outfile. class ReadOut: def __init__(self, fout=fout): self.file = open(fout, 'rb') self.first = True def cat(self): self.first = False for line in self.file: yield str(line[:-1], 'utf-8', 'ignore') def tac(self): if self.first: self.first = False self.file.seek(0, 2) buffer = b'' if self.file.tell(): self.file.seek(self.file.tell() - 1) if self.file.read(1) == b'\n': self.file.seek(self.file.tell() - 1) while self.file.tell(): self.file.seek(self.file.tell() - 1) char = self.file.read(1) if char == b'\n': yield str(buffer[::-1], 'utf-8', 'ignore') buffer = b'' else: buffer += char self.file.seek(self.file.tell() - 1) if buffer: yield str(buffer[::-1], 'utf-8', 'ignore') def close(self): self.file.close() # Define the functions! def build_replacer(replace): print('| Building replacement for:', replace) rep = [] # strings ref = [] # references maximum = 0 i = iter(replace) buf = '' for c in i: if c == '\\': try: c = next(i) except: return None if c == 'x': try: c = next(i) + next(i) except: print('| | Illegal "\\xNN", not enough characters?') return None try: buf += chr(int(c, 16)) except: buf += '\\x' + c elif c == 'u': try: c = next(i) + next(i) + next(i) + next(i) except: print('| | Illegal "\\uNNNN", not enough characters?') return None try: buf += chr(int(c, 16)) except: buf += '\\u' + c elif c.isdigit(): rep.append(buf) buf = '' c = int(c) ref.append((c,False)) if c > maximum: maximum = c elif c == '(': rep.append(buf) buf = '' for c in i: if c == ')': try: buf = int(buf) if buf < 0: print('| | Illegal group, less than zero:', buf) return None ref.append((buf, False)) if buf > maximum: maximum = buf except: ref.append((buf, False)) buf = '' else: buf += c if len(rep) != len(ref): print('| | Error, unbalanced parentheses to backreference.') # Unbalanced parens. return None elif c == ')': rep.append(buf) buf = '' for c in i: if c == '(': try: buf = int(buf) if buf < 0: print('| | Illegal group, less than zero:', buf) return None ref.append((buf, True)) if buf > maximum: maximum = buf except: ref.append((buf, True)) buf = '' else: buf += c if len(rep) != len(ref): print('| | Error, unbalanced parentheses to reverse of backreference.') # Unbalanced parens. return None elif c in '&\\/': buf += c else: buf += '\\' + c elif c == '&': rep.append(buf) buf = '' ref.append((0, False)) else: buf += c rep.append(buf) def replacer(zero, numbered, named): print('| Replacing...') if maximum > len(numbered): return None numbered = [zero] + list(numbered) result = '' for i, (code, rev) in enumerate(ref): result += rep[i] # add non-reference if isinstance(code, int): try: addition = numbered[code] except: print('| | Numbered group failed:', code) return None else: try: addition = named[bytes(code, 'utf-8')] except: print('| | Named group failed:', code) return None if addition is None: addition = '' elif rev: addition = addition[::-1] result += addition result += rep[-1] # add final non-reference print('| | ...success!') return result return replacer try: from re2 import compile as regex except ImportError: print('Warning: Using re and not re2.') from re import compile as regex sed_match = regex(r's/((?:\\.|[^/])*)/((?:\\.|[^/])*)/([^\s~]*)(?:~(\d+))?') def sed_test(nick, line): # Is it a command? match = sed_match.match(line) if not match: return None search, replace, who, back = match.groups() replace = build_replacer(replace) if not replace: return None return search, replace, nick, who, back def sed_method(search, replace, nick, who, back): # Some things to fix. if not back: back = 0 else: back = int(back) fuzzy = True if not who: fuzzy = False who = nick elif who == 'g': who = '' who = who.lower() # Turn it into a regex. try: search = regex(search) except: return None # Now it is time to try to sed. log = ReadOut() for line in log.tac(): _, _, nick, line = line.split(' ', 3) nick = nick[1:-1] skip = False for test, _ in methods: if test(nick, line): skip = True break if skip: continue action, nick, line = deirc(nick, line) match_nick = nick.lower().replace('*', '') if fuzzy and not match_nick.startswith(who): continue elif not fuzzy and match_nick != who: continue if action: action = False line = '\x01ACTION ' + line + '\x01' if not search.search(line): continue if back != 0: back -= 1 continue log.close() break else: log.close() return None result = '' prev = 0 for match in search.finditer(line): start, end = match.span() result += line[prev:start] prev = end replacement = replace(line[start:end], match.groups(), match.groupdict()) if replacement is None: return None result += replacement result += line[prev:] if result[:8] == '\x01ACTION ' and result[-1] == '\x01': action = True result = result[8:-1] log.close() return action, nick + '*', result find_match = regex(r'p([\+-]\d+)?/((?:\\.|[^/])*)/([^\s~]*)(?:~(\d+))?') def find_test(nick, line): # Is it a command? match = find_match.match(line) if not match: return None local, search, who, back = match.groups() return local, search, nick, who, back def find_method(local, search, nick, who, back): # Some things to fix. if not back: back = 0 else: back = int(back) fuzzy = True if not who: fuzzy = False who = nick elif who == 'g': who = '' who = who.lower() if not local: local = 0 else: local = int(local) # Turn it into a regex. search = regex(search) # Now it is time to try to sed. log = ReadOut() for line in log.tac(): _, _, nick, line = line.split(' ', 3) nick = nick[1:-1] skip = False for test, _ in methods: if test(nick, line): skip = True break if skip: continue action, nick, line = deirc(nick, line) match_nick = nick.lower().replace('*', '') if fuzzy and not match_nick.startswith(who): continue elif not fuzzy and match_nick != who: continue if action: match_line = '\x01ACTION ' + line + '\x01' else: match_line = line if not search.search(match_line): continue if back != 0: back -= 1 continue break else: log.close() return None if local == 0: log.close() return action, nick, line elif local > 0: for line in log.cat(): if local == 0: _, _, nick, line = line.split(' ', 3) return deirc(nick[1:-1], line) local -= 1 elif local < 0: for line in log.tac(): local += 1 if local == 0: _, _, nick, line = line.split(' ', 3) return deirc(nick[1:-1], line) # Should never happen. return None def tr_expand(string): i = iter(string) result = '' prev = '' for char in i: if prev == '\\' and char in '-\\/': prev = '' elif char == '-' and prev: a = ord(prev) b = None try: b = ord(next(i)) if a < b: prev = ''.join(map(chr, range(a, b + 1))) elif a > b: prev = ''.join(map(chr, reversed(range(b, a + 1)))) char = '' except: result += prev prev = char if b: char = chr(b) else: char = '' continue result += prev prev = char result += char return result tr_match = regex(r'y/((?:\\.|[^/])*)/((?:\\.|[^/])*)/([^\s~]*)(?:~(\d+))?') def tr_test(nick, line): # Is it a command? match = tr_match.match(line) if not match: return None start, finish, who, back = match.groups() start = tr_expand(start) if not start: return None finish = tr_expand(finish) if not finish: return None if len(start) != len(finish): return None return start, finish, nick, who, back def tr_method(start, finish, nick, who, back): # Some things to fix. translate = {a: b for (a, b) in zip(start, finish)} if not back: back = 0 else: back = int(back) fuzzy = True if not who: fuzzy = False who = nick elif who == 'g': who = '' who = who.lower() # Now it is time to try to tr. log = ReadOut() for line in log.tac(): _, _, nick, line = line.split(' ', 3) nick = nick[1:-1] skip = False for test, _ in methods: if test(nick, line): skip = True break if skip: continue action, nick, line = deirc(nick, line) match_nick = nick.lower().replace('*', '') if fuzzy and not match_nick.startswith(who): continue elif not fuzzy and match_nick != who: continue result = '' for c in line: result += translate.get(c, c) if result == line: continue if back != 0: back -= 1 continue log.close() break else: log.close() return None return action, nick + '*', result # Flags! Which do we enable? What do we append? for flag, test, method in ( ('s', sed_test, sed_method), ('p', find_test, find_method), ('y', tr_test, tr_method) ): if flag.lower() in flags: methods.append((test, method)) elif flag.upper() in flags: methods.append((test, lambda *a: None)) # Execute a command. from subprocess import Popen, PIPE def cmd(args): proc = Popen(args, stdout=PIPE) while True: line = proc.stdout.readline() if line: try: yield str(line[:-1], 'utf-8', 'ignore') except: pass else: break # Do the thing! from time import sleep def begin(): for line in cmd(['tail', '-n', '0', '-f', fout]): _, _, nick, line = line.split(' ', 3) nick = nick[1:-1] # Ignore actions. if line[:8] == '\x01ACTION ' and line[-1] == '\x01': continue # Ignore bots and nologs. if line[0] == '\u200b': continue if line.startswith('[nolog]') or line.startswith('nolog:'): continue # Try it out. for test, method in methods: print('Testing', method.__name__, 'with:', line) result = test(nick, line) if not result: print('| Test complete, yet invalid.') continue print('| Test complete and valid.\nProceeding with method', method.__name__ + '.') result = method(*result) print('| Method complete.') if not result: continue action, nick, line = result if action: reply = '* ' + nick + ' ' + line else: reply = '<' + nick + '> ' + line print('| It is valid! Sending:', reply) with open(fin, 'w') as fh: fh.write('\u200b' + reply + '\n') sleep(0.5) begin()