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