It's Bach. He's back. With more hearing than ever!

This commit is contained in:
zgrep 2018-08-14 03:39:35 -04:00
parent cb20e0e8de
commit b83e72fc37
4 changed files with 497 additions and 232 deletions

View File

@ -1,3 +1,5 @@
# happybot 2.0
# happybot <s>2.0</s> 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.
<s>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.</s>
Oh no. The old edition came back. And now it's _partially open source_! **The horror!!!**

230
xed.py
View File

@ -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 '<Parse error.>'
a = Expand().transform(a)
look = lookup(a)
locs = findall(look, s)
if not locs:
return '<No change.>'
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))

1
xed.py Symbolic link
View File

@ -0,0 +1 @@
xplace/x.py

230
xplace/x.py Normal file
View File

@ -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 '<Parse error.>'
a = Expand().transform(a)
look = lookup(a)
locs = findall(look, s)
if not locs:
return '<No change.>'
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))

262
xplace/xed.py Executable file
View File

@ -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()