From b83e72fc37f9bf2e5c08f91d870516dd33af5816 Mon Sep 17 00:00:00 2001 From: zgrep Date: Tue, 14 Aug 2018 03:39:35 -0400 Subject: [PATCH] It's Bach. He's back. With more hearing than ever! --- readme.md | 6 +- xed.py | 231 +------------------------------------------- xplace/x.py | 230 ++++++++++++++++++++++++++++++++++++++++++++ xplace/xed.py | 262 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 497 insertions(+), 232 deletions(-) mode change 100644 => 120000 xed.py create mode 100644 xplace/x.py create mode 100755 xplace/xed.py diff --git a/readme.md b/readme.md index 553c426..8be2c3f 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,5 @@ -# happybot 2.0 +# happybot 2.0 1.0 -You've heard the rumors, you know it's been a long time in the making, and it's finally here! Happybot 2.0: Vaporware Edition. +You've heard the rumors, you know it's been a long time in the making, and it's finally here! Happybot 2.0: Vaporware Edition. + +Oh no. The old edition came back. And now it's _partially open source_! **The horror!!!** diff --git a/xed.py b/xed.py deleted file mode 100644 index 9d6c584..0000000 --- a/xed.py +++ /dev/null @@ -1,230 +0,0 @@ -#!/usr/bin/env python3 -from lark import Lark, Transformer, ParseError, Tree - -parser = Lark(r''' - DIGIT : /[0-9]/ - DIGITS : DIGIT+ - NUMBER : "-"? DIGITS - -?bracketliteral : "\\" /./ - | /[^\]\-]/ - ?range : bracketliteral -> char - | bracketliteral "-" bracketliteral - brackets : "[" range+ "]" -> either - - char : /[^\\\[\]\{\}\(\)\|\^~\?!]/ - | "\\" /\D/ - | // - - numrange : DIGITS - | DIGITS "-" DIGITS - - ?unit : parens | brackets | char - ?concat_func : unit - | concat_func "{" DIGITS "}" -> concat_repeat - | concat_func "?" -> zero_or_one - | concat_func "~" -> reverse - | concat_func "~" NUMBER -> roll - | concat_func "~{" NUMBER ["," DIGITS] "}" -> roll - | concat_func "!" -> collapse - | concat_func "!" DIGIT+ -> collapse - | concat_func "!{" numrange ("," numrange)* "}" -> collapse_ranges - | concat_func "\\" DIGIT+ -> index - | concat_func "\\{" numrange ("," numrange)* "}" -> index_ranges - - ?concat : concat_func+ - - ?choice_func : concat - | choice_func "^{" DIGITS "}" -> weave_repeat - | choice_func "|{" DIGITS "}" -> either_repeat - - ?choice : choice_func - | choice ("^" choice_func)+ -> weave - | choice ("|" choice_func)+ -> either - - ?parens : "(" choice ")" - ''', start='choice', ambiguity='resolve__antiscore_sum') - -class Expand(Transformer): - def __init__(self, amp=None): - self.amp = amp - - def char(self, args): - if args: - c = args[0].value - else: - c = '' - if self.amp and c == '&': - return self.amp - return [c] - - def range(self, args): - result = [] - a, b = map(ord, args) - while a < b: - result.append(chr(a)) - a += 1 - while a > b: - result.append(chr(a)) - a -= 1 - result.append(chr(a)) - return result - - def zero_or_one(self, args): - return self.either([[''], args[0]]) - - def either(self, args): - result = [] - for x in args: - result.extend(x) - return result - - def concat(self, args): - result = [''] - for arg in args: - replace = [] - for a in result: - for b in arg: - replace.append(a + b) - result = replace - return result - - def weave(self, args): - result = [] - for i in range(max(map(len, args))): - for arg in args: - if i < len(arg): - result.append(arg[i]) - return result - - def roll(self, args): - if len(args) == 3: - g = int(args[2].value) - else: - g = len(args[0]) - r = int(args[1].value) - groups = [[]] - for i, elem in enumerate(args[0]): - if i % g == 0: - groups.append([]) - groups[-1].append(elem) - result = [] - for group in groups: - for i in range(len(group)): - result.append(group[(i + r) % len(group)]) - return result - - def reverse(self, args): - return args[0][::-1] - - def numrange(self, args): - result = [] - a = int(args[0].value) - if len(args) == 1: - b = a - else: - b = int(args[1].value) - while a < b: - result.append(a) - a += 1 - while a > b: - result.append(a) - a -= 1 - result.append(a) - return result - - def index(self, args): - result, x = [], args[0] - for i in args[1:]: - result.append(x[int(i.value) % len(x)]) - return result - - def index_ranges(self, args): - result, x = [], args[0] - for arg in args[1:]: - for i in arg: - result.append(x[i % len(x)]) - return result - - def collapse(self, args): - result, x = '', args[0] - if len(args) > 1: - for i in args[1:]: - result += x[int(i.value) % len(x)] - else: - result = ''.join(args[0]) - return [result] - - def collapse_ranges(self, args): - result, x = '', args[0] - for arg in args[1:]: - for i in arg: - result += x[i % len(x)] - return [result] - - - def concat_repeat(self, args): - return self.concat([args[0]] * int(args[1].value)) - def either_repeat(self, args): - return self.either([args[0]] * int(args[1].value)) - def weave_repeat(self, args): - return self.weave([args[0]] * int(args[1].value)) - -def lookup(choices): - lookup = dict() - for n, choice in enumerate(choices): - curr = lookup - for c in choice: - if c not in curr: - curr[c] = dict() - curr = curr[c] - curr[None] = n - return lookup - -def findall(lookup, string): - i, result = 0, [] - while i < len(string): - c = string[i] - if c in lookup: - j = i + 1 - curr = lookup[c] - while j < len(string) and string[j] in curr: - curr = curr[string[j]] - j += 1 - if None in curr: - result.append((curr[None], i, j)) - i = j - else: - i += 1 - elif None in lookup: - result.append((lookup[None], i, i)) - i += 1 - else: - i += 1 - if None in lookup: - i = len(string) - result.append((lookup[None], i, i)) - return result - -def replace(a, b, s): - try: - a = parser.parse(a) - b = parser.parse(b) - except ParseError: - return '' - a = Expand().transform(a) - look = lookup(a) - locs = findall(look, s) - if not locs: - return '' - b = Expand(amp=a).transform(b) - for n, i, j in reversed(locs): - r = b[n % len(b)] - s = s[:i] + r + s[j:] - return s - -if __name__ == '__main__': - from sys import argv - p = parser.parse(argv[1]) - print(p.pretty()) - print(Expand().transform(p)) diff --git a/xed.py b/xed.py new file mode 120000 index 0000000..1bf1b3f --- /dev/null +++ b/xed.py @@ -0,0 +1 @@ +xplace/x.py \ No newline at end of file diff --git a/xplace/x.py b/xplace/x.py new file mode 100644 index 0000000..9d6c584 --- /dev/null +++ b/xplace/x.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +from lark import Lark, Transformer, ParseError, Tree + +parser = Lark(r''' + DIGIT : /[0-9]/ + DIGITS : DIGIT+ + NUMBER : "-"? DIGITS + +?bracketliteral : "\\" /./ + | /[^\]\-]/ + ?range : bracketliteral -> char + | bracketliteral "-" bracketliteral + brackets : "[" range+ "]" -> either + + char : /[^\\\[\]\{\}\(\)\|\^~\?!]/ + | "\\" /\D/ + | // + + numrange : DIGITS + | DIGITS "-" DIGITS + + ?unit : parens | brackets | char + ?concat_func : unit + | concat_func "{" DIGITS "}" -> concat_repeat + | concat_func "?" -> zero_or_one + | concat_func "~" -> reverse + | concat_func "~" NUMBER -> roll + | concat_func "~{" NUMBER ["," DIGITS] "}" -> roll + | concat_func "!" -> collapse + | concat_func "!" DIGIT+ -> collapse + | concat_func "!{" numrange ("," numrange)* "}" -> collapse_ranges + | concat_func "\\" DIGIT+ -> index + | concat_func "\\{" numrange ("," numrange)* "}" -> index_ranges + + ?concat : concat_func+ + + ?choice_func : concat + | choice_func "^{" DIGITS "}" -> weave_repeat + | choice_func "|{" DIGITS "}" -> either_repeat + + ?choice : choice_func + | choice ("^" choice_func)+ -> weave + | choice ("|" choice_func)+ -> either + + ?parens : "(" choice ")" + ''', start='choice', ambiguity='resolve__antiscore_sum') + +class Expand(Transformer): + def __init__(self, amp=None): + self.amp = amp + + def char(self, args): + if args: + c = args[0].value + else: + c = '' + if self.amp and c == '&': + return self.amp + return [c] + + def range(self, args): + result = [] + a, b = map(ord, args) + while a < b: + result.append(chr(a)) + a += 1 + while a > b: + result.append(chr(a)) + a -= 1 + result.append(chr(a)) + return result + + def zero_or_one(self, args): + return self.either([[''], args[0]]) + + def either(self, args): + result = [] + for x in args: + result.extend(x) + return result + + def concat(self, args): + result = [''] + for arg in args: + replace = [] + for a in result: + for b in arg: + replace.append(a + b) + result = replace + return result + + def weave(self, args): + result = [] + for i in range(max(map(len, args))): + for arg in args: + if i < len(arg): + result.append(arg[i]) + return result + + def roll(self, args): + if len(args) == 3: + g = int(args[2].value) + else: + g = len(args[0]) + r = int(args[1].value) + groups = [[]] + for i, elem in enumerate(args[0]): + if i % g == 0: + groups.append([]) + groups[-1].append(elem) + result = [] + for group in groups: + for i in range(len(group)): + result.append(group[(i + r) % len(group)]) + return result + + def reverse(self, args): + return args[0][::-1] + + def numrange(self, args): + result = [] + a = int(args[0].value) + if len(args) == 1: + b = a + else: + b = int(args[1].value) + while a < b: + result.append(a) + a += 1 + while a > b: + result.append(a) + a -= 1 + result.append(a) + return result + + def index(self, args): + result, x = [], args[0] + for i in args[1:]: + result.append(x[int(i.value) % len(x)]) + return result + + def index_ranges(self, args): + result, x = [], args[0] + for arg in args[1:]: + for i in arg: + result.append(x[i % len(x)]) + return result + + def collapse(self, args): + result, x = '', args[0] + if len(args) > 1: + for i in args[1:]: + result += x[int(i.value) % len(x)] + else: + result = ''.join(args[0]) + return [result] + + def collapse_ranges(self, args): + result, x = '', args[0] + for arg in args[1:]: + for i in arg: + result += x[i % len(x)] + return [result] + + + def concat_repeat(self, args): + return self.concat([args[0]] * int(args[1].value)) + def either_repeat(self, args): + return self.either([args[0]] * int(args[1].value)) + def weave_repeat(self, args): + return self.weave([args[0]] * int(args[1].value)) + +def lookup(choices): + lookup = dict() + for n, choice in enumerate(choices): + curr = lookup + for c in choice: + if c not in curr: + curr[c] = dict() + curr = curr[c] + curr[None] = n + return lookup + +def findall(lookup, string): + i, result = 0, [] + while i < len(string): + c = string[i] + if c in lookup: + j = i + 1 + curr = lookup[c] + while j < len(string) and string[j] in curr: + curr = curr[string[j]] + j += 1 + if None in curr: + result.append((curr[None], i, j)) + i = j + else: + i += 1 + elif None in lookup: + result.append((lookup[None], i, i)) + i += 1 + else: + i += 1 + if None in lookup: + i = len(string) + result.append((lookup[None], i, i)) + return result + +def replace(a, b, s): + try: + a = parser.parse(a) + b = parser.parse(b) + except ParseError: + return '' + a = Expand().transform(a) + look = lookup(a) + locs = findall(look, s) + if not locs: + return '' + b = Expand(amp=a).transform(b) + for n, i, j in reversed(locs): + r = b[n % len(b)] + s = s[:i] + r + s[j:] + return s + +if __name__ == '__main__': + from sys import argv + p = parser.parse(argv[1]) + print(p.pretty()) + print(Expand().transform(p)) diff --git a/xplace/xed.py b/xplace/xed.py new file mode 100755 index 0000000..e4ff02c --- /dev/null +++ b/xplace/xed.py @@ -0,0 +1,262 @@ +#!/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) != 3: + print('Usage: in out') + exit(1) +_, fin, fout = argv + +# 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() + +from re import compile as regex + +xed_match = regex(r'x/((?:\\.|[^/])*)/((?:\\.|[^/])*)/([^\s~]*)(?:~(\d+))?') +xed_verbose_match = regex(r'xv/((?:\\.|[^/])*)/(?:((?:\\.|[^/])*)/)?') +sed_match = regex(r's/((?:\\.|[^/])*)/((?:\\.|[^/])*)/([^\s~]*)(?:~(\d+))?') +find_match = regex(r'p([\+-]\d+)?/((?:\\.|[^/])*)/([^\s~]*)(?:~(\d+))?') +tr_match = regex(r'y/((?:\\.|[^/])*)/((?:\\.|[^/])*)/([^\s~]*)(?:~(\d+))?') + +matchers = [xed_match, sed_match, find_match, tr_match, xed_verbose_match] + +def xed_test(nick, line): + # Is it a command? + match = xed_match.match(line) + if not match: + return None + search, replace, who, back = match.groups() + return search, replace, nick, who, back + +import x as xed + +def xed_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 possibility space. + try: + search = xed.parser.parse(search) + replace = xed.parser.parse(replace) + except e: + print('| Parsing error:', e) + return None + search = xed.Expand().transform(search) + lookup = xed.lookup(search) + + output = None + + # Now it is time to try to xed. + log = ReadOut() + for nline, line in enumerate(log.tac()): + if nline > 500: + log.close() + break + + _, _, nick, line = line.split(' ', 3) + nick = nick[1:-1] + + skip = False + for test in matchers: + if test.match(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' + + output = xed.findall(lookup, line) + if not output: + continue + + if back != 0: + back -= 1 + continue + + log.close() + break + else: + log.close() + return None + + if not output: + return None + + result = line + replace = xed.Expand(amp=search).transform(replace) + for n, i, j in reversed(output): + rep = replace[n % len(replace)] + result = result[:i] + rep + result[j:] + + if result[:8] == '\x01ACTION ' and result[-1] == '\x01': + action = True + result = result[8:-1] + + log.close() + return action, nick + '*', result + +# 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: + # Try it out. + print('Testing xed.') + result = xed_test(nick, line) + if not result: + print('| Test complete, yet invalid.') + m = xed_verbose_match.match(line) + if m: + n = m.group(2) + try: + m = xed.parser.parse(m.group(1)) + if n: + n = xed.parser.parse(n) + except: + with open(fin, 'w') as fh: + fh.write('\u200b' + nick + ': Parsing error.\n') + m = xed.Expand().transform(m) + if n: + n = xed.Expand(amp=m).transform(n) + with open(fin, 'w') as fh: + if n: + fh.write('\u200b' + nick + ': ' + repr(m) + ' -> ' + repr(n) + '\n') + else: + fh.write('\u200b' + nick + ': ' + repr(m) + '\n') + continue + print('| Test complete and valid.\nProceeding with xed.') + result = xed_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') + except: + with open(fin, 'w') as fh: + fh.write('\u200b' + nick + ': This would have crashed me.\n') + +begin()