Package rivescript ::
Module rivescript
|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 import sys
26 import os
27 import re
28 import string
29 import random
30 import pprint
31 import copy
32 import codecs
33
34 from . import __version__
35 from . import python
36
37
38 re_equals = re.compile('\s*=\s*')
39 re_ws = re.compile('\s+')
40 re_objend = re.compile('^\s*<\s*object')
41 re_weight = re.compile('\{weight=(\d+)\}')
42 re_inherit = re.compile('\{inherits=(\d+)\}')
43 re_wilds = re.compile('[\s\*\#\_]+')
44 re_nasties = re.compile('[^A-Za-z0-9 ]')
45
46
47 rs_version = 2.0
48
49
50 RS_ERR_MATCH = "ERR: No Reply Matched"
51 RS_ERR_REPLY = "ERR: No Reply Found"
55 """A RiveScript interpreter for Python 2 and 3."""
56
57
58
59
60
61 - def __init__(self, debug=False, strict=True, depth=50, log="", utf8=False):
62 """Initialize a new RiveScript interpreter.
63
64 bool debug: Specify a debug mode.
65 bool strict: Strict mode (RS syntax errors are fatal)
66 str log: Specify a log file for debug output to go to (instead of STDOUT).
67 int depth: Specify the recursion depth limit.
68 bool utf8: Enable UTF-8 support."""
69
70 self._debug = debug
71 self._log = log
72 self._utf8 = utf8
73 self._strict = strict
74 self._depth = depth
75 self._gvars = {}
76 self._bvars = {}
77 self._subs = {}
78 self._person = {}
79 self._arrays = {}
80 self._users = {}
81 self._freeze = {}
82 self._includes = {}
83 self._lineage = {}
84 self._handlers = {}
85 self._objlangs = {}
86 self._topics = {}
87 self._thats = {}
88 self._sorted = {}
89 self._syntax = {}
90
91
92 self._current_user = None
93
94
95 self._handlers["python"] = python.PyRiveObjects()
96
97 self._say("Interpreter initialized.")
98
99 @classmethod
101 """Return the version number of the RiveScript library.
102
103 This may be called as either a class method of a method of a RiveScript object."""
104 return __version__
105
106 - def _say(self, message):
107 if self._debug:
108 print("[RS] {}".format(message))
109 if self._log:
110
111 fh = open(self._log, 'a')
112 fh.write("[RS] " + message + "\n")
113 fh.close()
114
115 - def _warn(self, message, fname='', lineno=0):
116 header = "[RS]"
117 if self._debug:
118 header = "[RS::Warning]"
119 if len(fname) and lineno > 0:
120 print(header, message, "at", fname, "line", lineno)
121 else:
122 print(header, message)
123
124
125
126
127
129 """Load RiveScript documents from a directory.
130
131 Provide `ext` as a list of extensions to search for. The default list
132 is `.rive`, `.rs`"""
133 self._say("Loading from directory: " + directory)
134
135 if ext is None:
136
137 ext = ['.rive', '.rs']
138 elif type(ext) == str:
139
140 ext = [ext]
141
142 if not os.path.isdir(directory):
143 self._warn("Error: " + directory + " is not a directory.")
144 return
145
146 for item in os.listdir(directory):
147 for extension in ext:
148 if item.lower().endswith(extension):
149
150 self.load_file(os.path.join(directory, item))
151 break
152
154 """Load and parse a RiveScript document."""
155 self._say("Loading file: " + filename)
156
157 fh = codecs.open(filename, 'r', 'utf-8')
158 lines = fh.readlines()
159 fh.close()
160
161 self._say("Parsing " + str(len(lines)) + " lines of code from " + filename)
162 self._parse(filename, lines)
163
165 """Stream in RiveScript source code dynamically.
166
167 `code` should be an array of lines of RiveScript code."""
168 self._say("Streaming code.")
169 self._parse("stream()", code)
170
171 - def _parse(self, fname, code):
172 """Parse RiveScript code into memory."""
173 self._say("Parsing code")
174
175
176 topic = 'random'
177 lineno = 0
178 comment = False
179 inobj = False
180 objname = ''
181 objlang = ''
182 objbuf = []
183 ontrig = ''
184 repcnt = 0
185 concnt = 0
186 lastcmd = ''
187 isThat = ''
188
189
190 for lp, line in enumerate(code):
191 lineno = lineno + 1
192
193 self._say("Line: " + line + " (topic: " + topic + ") incomment: " + str(inobj))
194 if len(line.strip()) == 0:
195 continue
196
197
198 if inobj:
199 if re.match(re_objend, line):
200
201 if len(objname):
202
203 if objlang in self._handlers:
204 self._objlangs[objname] = objlang
205 self._handlers[objlang].load(objname, objbuf)
206 else:
207 self._warn("Object creation failed: no handler for " + objlang, fname, lineno)
208 objname = ''
209 objlang = ''
210 objbuf = []
211 inobj = False
212 else:
213 objbuf.append(line)
214 continue
215
216 line = line.strip()
217
218
219
220 if line[:2] == '//':
221 continue
222 elif line[0] == '#':
223 self._warn("Using the # symbol for comments is deprecated", fname, lineno)
224 elif line[:2] == '/*':
225 if not '*/' in line:
226 comment = True
227 continue
228 elif '*/' in line:
229 comment = False
230 continue
231 if comment:
232 continue
233
234
235 if len(line) < 2:
236 self._warn("Weird single-character line '" + line + "' found.", fname, lineno)
237 continue
238 cmd = line[0]
239 line = line[1:].strip()
240
241
242
243 if " // " in line:
244 line = line.split(" // ")[0].strip()
245
246
247 syntax_error = self.check_syntax(cmd, line)
248 if syntax_error:
249
250 syntax_error = "Syntax error in " + fname + " line " + str(lineno) + ": " \
251 + syntax_error + " (near: " + cmd + " " + line + ")"
252 if self._strict:
253 raise Exception(syntax_error)
254 else:
255 self._warn(syntax_error)
256 return
257
258
259 if cmd == '+':
260 isThat = ''
261
262
263 for i in range(lp + 1, len(code)):
264 lookahead = code[i].strip()
265 if len(lookahead) < 2:
266 continue
267 lookCmd = lookahead[0]
268 lookahead = lookahead[1:].strip()
269
270
271 if len(lookahead) != 0:
272
273 if lookCmd != '^' and lookCmd != '%':
274 break
275
276
277
278 if cmd == '+':
279 if lookCmd == '%':
280 isThat = lookahead
281 break
282 else:
283 isThat = ''
284
285
286
287
288 if cmd == '!':
289 if lookCmd == '^':
290 line += "<crlf>" + lookahead
291 continue
292
293
294
295
296 if cmd != '^' and lookCmd != '%':
297 if lookCmd == '^':
298 line += lookahead
299 else:
300 break
301
302 self._say("Command: " + cmd + "; line: " + line)
303
304
305 if cmd == '!':
306
307 halves = re.split(re_equals, line, 2)
308 left = re.split(re_ws, halves[0].strip(), 2)
309 value, type, var = '', '', ''
310 if len(halves) == 2:
311 value = halves[1].strip()
312 if len(left) >= 1:
313 type = left[0].strip()
314 if len(left) >= 2:
315 var = ' '.join(left[1:]).strip()
316
317
318 if type != 'array':
319 value = re.sub(r'<crlf>', '', value)
320
321
322 if type == 'version':
323
324 try:
325 if float(value) > rs_version:
326 self._warn("Unsupported RiveScript version. We only support " + rs_version, fname, lineno)
327 return
328 except:
329 self._warn("Error parsing RiveScript version number: not a number", fname, lineno)
330 continue
331
332
333 if len(var) == 0:
334 self._warn("Undefined variable name", fname, lineno)
335 continue
336 elif len(value) == 0:
337 self._warn("Undefined variable value", fname, lineno)
338 continue
339
340
341 if type == 'global':
342
343 self._say("\tSet global " + var + " = " + value)
344
345 if value == '<undef>':
346 try:
347 del(self._gvars[var])
348 except:
349 self._warn("Failed to delete missing global variable", fname, lineno)
350 else:
351 self._gvars[var] = value
352
353
354 if var == 'debug':
355 if value.lower() == 'true':
356 value = True
357 else:
358 value = False
359 self._debug = value
360 elif var == 'depth':
361 try:
362 self._depth = int(value)
363 except:
364 self._warn("Failed to set 'depth' because the value isn't a number!", fname, lineno)
365 elif var == 'strict':
366 if value.lower() == 'true':
367 self._strict = True
368 else:
369 self._strict = False
370 elif type == 'var':
371
372 self._say("\tSet bot variable " + var + " = " + value)
373
374 if value == '<undef>':
375 try:
376 del(self._bvars[var])
377 except:
378 self._warn("Failed to delete missing bot variable", fname, lineno)
379 else:
380 self._bvars[var] = value
381 elif type == 'array':
382
383 self._say("\tArray " + var + " = " + value)
384
385 if value == '<undef>':
386 try:
387 del(self._arrays[var])
388 except:
389 self._warn("Failed to delete missing array", fname, lineno)
390 continue
391
392
393 parts = value.split("<crlf>")
394
395
396 fields = []
397 for val in parts:
398 if '|' in val:
399 fields.extend(val.split('|'))
400 else:
401 fields.extend(re.split(re_ws, val))
402
403
404 for f in fields:
405 f = f.replace(r'\s', ' ')
406
407 self._arrays[var] = fields
408 elif type == 'sub':
409
410 self._say("\tSubstitution " + var + " => " + value)
411
412 if value == '<undef>':
413 try:
414 del(self._subs[var])
415 except:
416 self._warn("Failed to delete missing substitution", fname, lineno)
417 else:
418 self._subs[var] = value
419 elif type == 'person':
420
421 self._say("\tPerson Substitution " + var + " => " + value)
422
423 if value == '<undef>':
424 try:
425 del(self._person[var])
426 except:
427 self._warn("Failed to delete missing person substitution", fname, lineno)
428 else:
429 self._person[var] = value
430 else:
431 self._warn("Unknown definition type '" + type + "'", fname, lineno)
432 elif cmd == '>':
433
434 temp = re.split(re_ws, line)
435 type = temp[0]
436 name = ''
437 fields = []
438 if len(temp) >= 2:
439 name = temp[1]
440 if len(temp) >= 3:
441 fields = temp[2:]
442
443
444 if type == 'begin':
445
446 self._say("\tFound the BEGIN block.")
447 type = 'topic'
448 name = '__begin__'
449 if type == 'topic':
450
451 self._say("\tSet topic to " + name)
452 ontrig = ''
453 topic = name
454
455
456 mode = ''
457 if len(fields) >= 2:
458 for field in fields:
459 if field == 'includes':
460 mode = 'includes'
461 elif field == 'inherits':
462 mode = 'inherits'
463 elif mode != '':
464
465 if mode == 'includes':
466 if not name in self._includes:
467 self._includes[name] = {}
468 self._includes[name][field] = 1
469 else:
470 if not name in self._lineage:
471 self._lineage[name] = {}
472 self._lineage[name][field] = 1
473 elif type == 'object':
474
475
476 lang = None
477 if len(fields) > 0:
478 lang = fields[0].lower()
479
480
481 ontrig = ''
482 if lang is None:
483 self._warn("Trying to parse unknown programming language", fname, lineno)
484 lang = 'python'
485
486
487 if lang in self._handlers:
488
489 objname = name
490 objlang = lang
491 objbuf = []
492 inobj = True
493 else:
494
495 objname = ''
496 objlang = ''
497 objbuf = []
498 inobj = True
499 else:
500 self._warn("Unknown label type '" + type + "'", fname, lineno)
501 elif cmd == '<':
502
503 type = line
504
505 if type == 'begin' or type == 'topic':
506 self._say("\tEnd topic label.")
507 topic = 'random'
508 elif type == 'object':
509 self._say("\tEnd object label.")
510 inobj = False
511 elif cmd == '+':
512
513 self._say("\tTrigger pattern: " + line)
514 if len(isThat):
515 self._initTT('thats', topic, isThat, line)
516 self._initTT('syntax', topic, line, 'thats')
517 self._syntax['thats'][topic][line]['trigger'] = (fname, lineno)
518 else:
519 self._initTT('topics', topic, line)
520 self._initTT('syntax', topic, line, 'topic')
521 self._syntax['topic'][topic][line]['trigger'] = (fname, lineno)
522 ontrig = line
523 repcnt = 0
524 concnt = 0
525 elif cmd == '-':
526
527 if ontrig == '':
528 self._warn("Response found before trigger", fname, lineno)
529 continue
530 self._say("\tResponse: " + line)
531 if len(isThat):
532 self._thats[topic][isThat][ontrig]['reply'][repcnt] = line
533 self._syntax['thats'][topic][ontrig]['reply'][repcnt] = (fname, lineno)
534 else:
535 self._topics[topic][ontrig]['reply'][repcnt] = line
536 self._syntax['topic'][topic][ontrig]['reply'][repcnt] = (fname, lineno)
537 repcnt = repcnt + 1
538 elif cmd == '%':
539
540 pass
541 elif cmd == '^':
542
543 pass
544 elif cmd == '@':
545
546 self._say("\tRedirect response to " + line)
547 if len(isThat):
548 self._thats[topic][isThat][ontrig]['redirect'] = line
549 self._syntax['thats'][topic][ontrig]['redirect'] = (fname, lineno)
550 else:
551 self._topics[topic][ontrig]['redirect'] = line
552 self._syntax['topic'][topic][ontrig]['redirect'] = (fname, lineno)
553 elif cmd == '*':
554
555 self._say("\tAdding condition: " + line)
556 if len(isThat):
557 self._thats[topic][isThat][ontrig]['condition'][concnt] = line
558 self._syntax['thats'][topic][ontrig]['condition'][concnt] = (fname, lineno)
559 else:
560 self._topics[topic][ontrig]['condition'][concnt] = line
561 self._syntax['topic'][topic][ontrig]['condition'][concnt] = (fname, lineno)
562 concnt = concnt + 1
563 else:
564 self._warn("Unrecognized command \"" + cmd + "\"", fname, lineno)
565 continue
566
568 """Syntax check a RiveScript command and line.
569
570 Returns a syntax error string on error; None otherwise."""
571
572
573 if cmd == '!':
574
575
576
577
578
579 match = re.match(r'^.+(?:\s+.+|)\s*=\s*.+?$', line)
580 if not match:
581 return "Invalid format for !Definition line: must be '! type name = value' OR '! type = value'"
582 elif cmd == '>':
583
584
585
586
587 parts = re.split(" ", line, 2)
588 if parts[0] == "begin" and len(parts) > 1:
589 return "The 'begin' label takes no additional arguments, should be verbatim '> begin'"
590 elif parts[0] == "topic":
591 rest = ' '.join(parts)
592 match = re.match(r'[^a-z0-9_\-\s]', line)
593 if match:
594 return "Topics should be lowercased and contain only numbers and letters"
595 elif parts[0] == "object":
596 rest = ' '.join(parts)
597 match = re.match(r'[^A-Za-z0-9_\-\s]', line)
598 if match:
599 return "Objects can only contain numbers and letters"
600 elif cmd == '+' or cmd == '%' or cmd == '@':
601
602
603
604
605
606
607 parens = 0
608 square = 0
609 curly = 0
610 angle = 0
611
612
613 for char in line:
614 if char == '(':
615 parens = parens + 1
616 elif char == ')':
617 parens = parens - 1
618 elif char == '[':
619 square = square + 1
620 elif char == ']':
621 square = square - 1
622 elif char == '{':
623 curly = curly + 1
624 elif char == '}':
625 curly = curly - 1
626 elif char == '<':
627 angle = angle + 1
628 elif char == '>':
629 angle = angle - 1
630
631
632 if parens != 0:
633 return "Unmatched parenthesis brackets"
634 elif square != 0:
635 return "Unmatched square brackets"
636 elif curly != 0:
637 return "Unmatched curly brackets"
638 elif angle != 0:
639 return "Unmatched angle brackets"
640
641
642 if self._utf8:
643 match = re.match(r'[A-Z\\.]', line)
644 if match:
645 return "Triggers can't contain uppercase letters, backslashes or dots in UTF-8 mode."
646 else:
647 match = re.match(r'[^a-z0-9(\|)\[\]*_#@{}<>=\s]', line)
648 if match:
649 return "Triggers may only contain lowercase letters, numbers, and these symbols: ( | ) [ ] * _ # @ { } < > ="
650 elif cmd == '-' or cmd == '^' or cmd == '/':
651
652
653 pass
654 elif cmd == '*':
655
656
657
658 match = re.match(r'^.+?\s*(?:==|eq|!=|ne|<>|<|<=|>|>=)\s*.+?=>.+?$', line)
659 if not match:
660 return "Invalid format for !Condition: should be like '* value symbol value => response'"
661
662 return None
663
665 """Return the in-memory RiveScript document as a Python data structure.
666
667 This would be useful for developing a user interface for editing
668 RiveScript replies without having to edit the RiveScript code
669 manually."""
670
671
672 result = {
673 "begin": {
674 "global": {},
675 "var": {},
676 "sub": {},
677 "person": {},
678 "array": {},
679 "triggers": {},
680 "that": {},
681 },
682 "topic": {},
683 "that": {},
684 "inherit": {},
685 "include": {},
686 }
687
688
689 if self._debug:
690 result["begin"]["global"]["debug"] = self._debug
691 if self._depth != 50:
692 result["begin"]["global"]["depth"] = 50
693
694
695 result["begin"]["var"] = self._bvars.copy()
696 result["begin"]["sub"] = self._subs.copy()
697 result["begin"]["person"] = self._person.copy()
698 result["begin"]["array"] = self._arrays.copy()
699 result["begin"]["global"].update(self._gvars.copy())
700
701
702 for topic in self._topics:
703 dest = {}
704
705 if topic == "__begin__":
706
707 dest = result["begin"]["triggers"]
708 else:
709
710 if not topic in result["topic"]:
711 result["topic"][topic] = {}
712 dest = result["topic"][topic]
713
714
715 for trig, data in self._topics[topic].iteritems():
716 dest[trig] = self._copy_trigger(trig, data)
717
718
719 for topic in self._thats:
720 dest = {}
721
722 if topic == "__begin__":
723
724 dest = result["begin"]["that"]
725 else:
726
727 if not topic in result["that"]:
728 result["that"][topic] = {}
729 dest = result["that"][topic]
730
731
732 for previous, pdata in self._thats[topic].iteritems():
733 for trig, data in pdata.iteritems():
734 dest[trig] = self._copy_trigger(trig, data, previous)
735
736
737 for topic, data in self._lineage.iteritems():
738 result["inherit"][topic] = []
739 for inherit in data:
740 result["inherit"][topic].append(inherit)
741 for topic, data in self._includes.iteritems():
742 result["include"][topic] = []
743 for include in data:
744 result["include"][topic].append(include)
745
746 return result
747
748 - def write(self, fh, deparsed=None):
749 """Write the currently parsed RiveScript data into a file.
750
751 Pass either a file name (string) or a file handle object.
752
753 This uses `deparse()` to dump a representation of the loaded data and
754 writes it to the destination file. If you provide your own data as the
755 `deparsed` argument, it will use that data instead of calling
756 `deparse()` itself. This way you can use `deparse()`, edit the data,
757 and use that to write the RiveScript document (for example, to be used
758 by a user interface for editing RiveScript without writing the code
759 directly)."""
760
761
762 if type(fh) is str:
763 fh = codecs.open(fh, "w", "utf-8")
764
765
766 if deparsed is None:
767 deparsed = self.deparse()
768
769
770 fh.write("// Written by rivescript.deparse()\n")
771 fh.write("! version = 2.0\n\n")
772
773
774 for kind in ["global", "var", "sub", "person", "array"]:
775 if len(deparsed["begin"][kind].keys()) == 0:
776 continue
777
778 for var in sorted(deparsed["begin"][kind].keys()):
779
780 data = deparsed["begin"][kind][var]
781 if type(data) not in [str, unicode]:
782 needs_pipes = False
783 for test in data:
784 if " " in test:
785 needs_pipes = True
786 break
787
788
789
790 width = 78 - len(kind) - len(var) - 4
791
792 if needs_pipes:
793 data = self._write_wrapped("|".join(data), sep="|")
794 else:
795 data = " ".join(data)
796
797 fh.write("! {kind} {var} = {data}\n".format(
798 kind=kind,
799 var=var,
800 data=data,
801 ))
802 fh.write("\n")
803
804
805 if len(deparsed["begin"]["triggers"].keys()):
806 fh.write("> begin\n\n")
807 self._write_triggers(fh, deparsed["begin"]["triggers"], indent="\t")
808 fh.write("< begin\n\n")
809
810
811 topics = ["random"]
812 topics.extend(sorted(deparsed["topic"].keys()))
813 done_random = False
814 for topic in topics:
815 if not topic in deparsed["topic"]: continue
816 if topic == "random" and done_random: continue
817 if topic == "random": done_random = True
818
819 tagged = False
820
821 if topic != "random" or topic in deparsed["include"] or topic in deparsed["inherit"]:
822 tagged = True
823 fh.write("> topic " + topic)
824
825 if topic in deparsed["inherit"]:
826 fh.write(" inherits " + " ".join(deparsed["inherit"][topic]))
827 if topic in deparsed["include"]:
828 fh.write(" includes " + " ".join(deparsed["include"][topic]))
829
830 fh.write("\n\n")
831
832 indent = "\t" if tagged else ""
833 self._write_triggers(fh, deparsed["topic"][topic], indent=indent)
834
835
836 if topic in deparsed["that"]:
837 self._write_triggers(fh, deparsed["that"][topic], indent=indent)
838
839 if tagged:
840 fh.write("< topic\n\n")
841
842 return True
843
845 """Make copies of all data below a trigger."""
846
847 dest = {}
848
849 if previous:
850 dest["previous"] = previous
851
852 if "redirect" in data and data["redirect"]:
853
854 dest["redirect"] = data["redirect"]
855
856 if "condition" in data and len(data["condition"].keys()):
857
858 dest["condition"] = []
859 for i in sorted(data["condition"].keys()):
860 dest["condition"].append(data["condition"][i])
861
862 if "reply" in data and len(data["reply"].keys()):
863
864 dest["reply"] = []
865 for i in sorted(data["reply"].keys()):
866 dest["reply"].append(data["reply"][i])
867
868 return dest
869
871 """Write triggers to a file handle."""
872
873 for trig in sorted(triggers.keys()):
874 fh.write(indent + "+ " + self._write_wrapped(trig, indent=indent) + "\n")
875 d = triggers[trig]
876
877 if "previous" in d:
878 fh.write(indent + "% " + self._write_wrapped(d["previous"], indent=indent) + "\n")
879
880 if "condition" in d:
881 for cond in d["condition"]:
882 fh.write(indent + "* " + self._write_wrapped(cond, indent=indent) + "\n")
883
884 if "redirect" in d:
885 fh.write(indent + "@ " + self._write_wrapped(d["redirect"], indent=indent) + "\n")
886
887 if "reply" in d:
888 for reply in d["reply"]:
889 fh.write(indent + "- " + self._write_wrapped(reply, indent=indent) + "\n")
890
891 fh.write("\n")
892
894 """Word-wrap a line of RiveScript code for being written to a file."""
895
896 words = line.split(sep)
897 lines = []
898 line = ""
899 buf = []
900
901 while len(words):
902 buf.append(words.pop(0))
903 line = sep.join(buf)
904 if len(line) > width:
905
906 words.insert(0, buf.pop())
907 lines.append(sep.join(buf))
908 buf = []
909 line = ""
910
911
912 if line:
913 lines.append(line)
914
915
916 result = lines.pop(0)
917 if len(lines):
918 eol = ""
919 if sep == " ":
920 eol = "\s"
921 for item in lines:
922 result += eol + "\n" + indent + "^ " + item
923
924 return result
925
926 - def _initTT(self, toplevel, topic, trigger, what=''):
927 """Initialize a Topic Tree data structure."""
928 if toplevel == 'topics':
929 if not topic in self._topics:
930 self._topics[topic] = {}
931 if not trigger in self._topics[topic]:
932 self._topics[topic][trigger] = {}
933 self._topics[topic][trigger]['reply'] = {}
934 self._topics[topic][trigger]['condition'] = {}
935 self._topics[topic][trigger]['redirect'] = None
936 elif toplevel == 'thats':
937 if not topic in self._thats:
938 self._thats[topic] = {}
939 if not trigger in self._thats[topic]:
940 self._thats[topic][trigger] = {}
941 if not what in self._thats[topic][trigger]:
942 self._thats[topic][trigger][what] = {}
943 self._thats[topic][trigger][what]['reply'] = {}
944 self._thats[topic][trigger][what]['condition'] = {}
945 self._thats[topic][trigger][what]['redirect'] = {}
946 elif toplevel == 'syntax':
947 if not what in self._syntax:
948 self._syntax[what] = {}
949 if not topic in self._syntax[what]:
950 self._syntax[what][topic] = {}
951 if not trigger in self._syntax[what][topic]:
952 self._syntax[what][topic][trigger] = {}
953 self._syntax[what][topic][trigger]['reply'] = {}
954 self._syntax[what][topic][trigger]['condition'] = {}
955 self._syntax[what][topic][trigger]['redirect'] = {}
956
957
958
959
960
962 """Sort the loaded triggers."""
963
964 triglvl = None
965 sortlvl = None
966 if thats:
967 triglvl = self._thats
968 sortlvl = 'thats'
969 else:
970 triglvl = self._topics
971 sortlvl = 'topics'
972
973
974 self._sorted[sortlvl] = {}
975
976 self._say("Sorting triggers...")
977
978
979 for topic in triglvl:
980 self._say("Analyzing topic " + topic)
981
982
983
984
985 alltrig = self._topic_triggers(topic, triglvl)
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004 running = self._sort_trigger_set(alltrig)
1005
1006
1007 if not sortlvl in self._sorted:
1008 self._sorted[sortlvl] = {}
1009 self._sorted[sortlvl][topic] = running
1010
1011
1012 if not thats:
1013
1014 self.sort_replies(True)
1015
1016
1017
1018
1019 self._sort_that_triggers()
1020
1021
1022 self._sort_list('subs', self._subs)
1023 self._sort_list('person', self._person)
1024
1026 """Make a sorted list of triggers that correspond to %Previous groups."""
1027 self._say("Sorting reverse triggers for %Previous groups...")
1028
1029 if not "that_trig" in self._sorted:
1030 self._sorted["that_trig"] = {}
1031
1032 for topic in self._thats:
1033 if not topic in self._sorted["that_trig"]:
1034 self._sorted["that_trig"][topic] = {}
1035
1036 for bottrig in self._thats[topic]:
1037 if not bottrig in self._sorted["that_trig"][topic]:
1038 self._sorted["that_trig"][topic][bottrig] = []
1039 triggers = self._sort_trigger_set(self._thats[topic][bottrig].keys())
1040 self._sorted["that_trig"][topic][bottrig] = triggers
1041
1043 """Sort a group of triggers in optimal sorting order."""
1044
1045
1046 prior = {
1047 0: []
1048 }
1049
1050 for trig in triggers:
1051 match, weight = re.search(re_weight, trig), 0
1052 if match:
1053 weight = int(match.group(1))
1054 if not weight in prior:
1055 prior[weight] = []
1056
1057 prior[weight].append(trig)
1058
1059
1060 running = []
1061
1062
1063 for p in sorted(prior.keys(), reverse=True):
1064 self._say("\tSorting triggers with priority " + str(p))
1065
1066
1067
1068
1069 inherits = -1
1070 highest_inherits = -1
1071
1072
1073 track = {
1074 inherits: self._init_sort_track()
1075 }
1076
1077 for trig in prior[p]:
1078 self._say("\t\tLooking at trigger: " + trig)
1079
1080
1081 match = re.search(re_inherit, trig)
1082 if match:
1083 inherits = int(match.group(1))
1084 if inherits > highest_inherits:
1085 highest_inherits = inherits
1086 self._say("\t\t\tTrigger belongs to a topic which inherits other topics: level=" + str(inherits))
1087 trig = re.sub(re_inherit, "", trig)
1088 else:
1089 inherits = -1
1090
1091
1092
1093 if not inherits in track:
1094 track[inherits] = self._init_sort_track()
1095
1096
1097 if '_' in trig:
1098
1099 cnt = self._word_count(trig)
1100 self._say("\t\t\tHas a _ wildcard with " + str(cnt) + " words.")
1101 if cnt > 1:
1102 if not cnt in track[inherits]['alpha']:
1103 track[inherits]['alpha'][cnt] = []
1104 track[inherits]['alpha'][cnt].append(trig)
1105 else:
1106 track[inherits]['under'].append(trig)
1107 elif '#' in trig:
1108
1109 cnt = self._word_count(trig)
1110 self._say("\t\t\tHas a # wildcard with " + str(cnt) + " words.")
1111 if cnt > 1:
1112 if not cnt in track[inherits]['number']:
1113 track[inherits]['number'][cnt] = []
1114 track[inherits]['number'][cnt].append(trig)
1115 else:
1116 track[inherits]['pound'].append(trig)
1117 elif '*' in trig:
1118
1119 cnt = self._word_count(trig)
1120 self._say("\t\t\tHas a * wildcard with " + str(cnt) + " words.")
1121 if cnt > 1:
1122 if not cnt in track[inherits]['wild']:
1123 track[inherits]['wild'][cnt] = []
1124 track[inherits]['wild'][cnt].append(trig)
1125 else:
1126 track[inherits]['star'].append(trig)
1127 elif '[' in trig:
1128
1129 cnt = self._word_count(trig)
1130 self._say("\t\t\tHas optionals and " + str(cnt) + " words.")
1131 if not cnt in track[inherits]['option']:
1132 track[inherits]['option'][cnt] = []
1133 track[inherits]['option'][cnt].append(trig)
1134 else:
1135
1136 cnt = self._word_count(trig)
1137 self._say("\t\t\tTotally atomic and " + str(cnt) + " words.")
1138 if not cnt in track[inherits]['atomic']:
1139 track[inherits]['atomic'][cnt] = []
1140 track[inherits]['atomic'][cnt].append(trig)
1141
1142
1143 track[highest_inherits + 1] = track[-1]
1144 del(track[-1])
1145
1146
1147 for ip in sorted(track.keys()):
1148 self._say("ip=" + str(ip))
1149 for kind in ['atomic', 'option', 'alpha', 'number', 'wild']:
1150 for i in sorted(track[ip][kind], reverse=True):
1151 running.extend(track[ip][kind][i])
1152 running.extend(sorted(track[ip]['under'], key=len, reverse=True))
1153 running.extend(sorted(track[ip]['pound'], key=len, reverse=True))
1154 running.extend(sorted(track[ip]['star'], key=len, reverse=True))
1155 return running
1156
1158 """Sort a simple list by number of words and length."""
1159
1160 def by_length(word1, word2):
1161 return len(word2) - len(word1)
1162
1163
1164 if not "lists" in self._sorted:
1165 self._sorted["lists"] = {}
1166 self._sorted["lists"][name] = []
1167
1168
1169 track = {}
1170
1171
1172 for item in items:
1173
1174 cword = self._word_count(item, all=True)
1175 if not cword in track:
1176 track[cword] = []
1177 track[cword].append(item)
1178
1179
1180 output = []
1181 for count in sorted(track.keys(), reverse=True):
1182 sort = sorted(track[count], key=len, reverse=True)
1183 output.extend(sort)
1184
1185 self._sorted["lists"][name] = output
1186
1188 """Returns a new dict for keeping track of triggers for sorting."""
1189 return {
1190 'atomic': {},
1191 'option': {},
1192 'alpha': {},
1193 'number': {},
1194 'wild': {},
1195 'pound': [],
1196 'under': [],
1197 'star': []
1198 }
1199
1200
1201
1202
1203
1204
1206 """Define a custom language handler for RiveScript objects.
1207
1208 language: The lowercased name of the programming language,
1209 e.g. python, javascript, perl
1210 obj: An instance of a class object that provides the following interface:
1211
1212 class MyObjectHandler:
1213 def __init__(self):
1214 pass
1215 def load(self, name, code):
1216 # name = the name of the object from the RiveScript code
1217 # code = the source code of the object
1218 def call(self, rs, name, fields):
1219 # rs = the current RiveScript interpreter object
1220 # name = the name of the object being called
1221 # fields = array of arguments passed to the object
1222 return reply
1223
1224 Pass in a None value for the object to delete an existing handler (for example,
1225 to prevent Python code from being able to be run by default).
1226
1227 Look in the `eg` folder of the rivescript-python distribution for an example
1228 script that sets up a JavaScript language handler."""
1229
1230
1231 if obj is None:
1232 if language in self._handlers:
1233 del self._handlers[language]
1234 else:
1235 self._handlers[language] = obj
1236
1238 """Define a Python object from your program.
1239
1240 This is equivalent to having an object defined in the RiveScript code, except
1241 your Python code is defining it instead. `name` is the name of the object, and
1242 `code` is a Python function (a `def`) that accepts rs,args as its parameters.
1243
1244 This method is only available if there is a Python handler set up (which there
1245 is by default, unless you've called set_handler("python", None))."""
1246
1247
1248 if 'python' in self._handlers:
1249 self._handlers['python']._objects[name] = code
1250 else:
1251 self._warn("Can't set_subroutine: no Python object handler!")
1252
1254 """Set a global variable.
1255
1256 Equivalent to `! global` in RiveScript code. Set to None to delete."""
1257 if value is None:
1258
1259 if name in self._gvars:
1260 del self._gvars[name]
1261 self._gvars[name] = value
1262
1264 """Set a bot variable.
1265
1266 Equivalent to `! var` in RiveScript code. Set to None to delete."""
1267 if value is None:
1268
1269 if name in self._bvars:
1270 del self._bvars[name]
1271 self._bvars[name] = value
1272
1274 """Set a substitution.
1275
1276 Equivalent to `! sub` in RiveScript code. Set to None to delete."""
1277 if rep is None:
1278
1279 if what in self._subs:
1280 del self._subs[what]
1281 self._subs[what] = rep
1282
1284 """Set a person substitution.
1285
1286 Equivalent to `! person` in RiveScript code. Set to None to delete."""
1287 if rep is None:
1288
1289 if what in self._person:
1290 del self._person[what]
1291 self._person[what] = rep
1292
1294 """Set a variable for a user."""
1295
1296 if not user in self._users:
1297 self._users[user] = {"topic": "random"}
1298
1299 self._users[user][name] = value
1300
1302 """Get a variable about a user.
1303
1304 If the user has no data at all, returns None. If the user doesn't have a value
1305 set for the variable you want, returns the string 'undefined'."""
1306
1307 if user in self._users:
1308 if name in self._users[user]:
1309 return self._users[user][name]
1310 else:
1311 return "undefined"
1312 else:
1313 return None
1314
1316 """Get all variables about a user (or all users).
1317
1318 If no username is passed, returns the entire user database structure. Otherwise,
1319 only returns the variables for the given user, or None if none exist."""
1320
1321 if user is None:
1322
1323 return self._users
1324 elif user in self._users:
1325
1326 return self._users[user]
1327 else:
1328
1329 return None
1330
1332 """Delete all variables about a user (or all users).
1333
1334 If no username is passed, deletes all variables about all users. Otherwise, only
1335 deletes all variables for the given user."""
1336
1337 if user is None:
1338
1339 self._users = {}
1340 elif user in self._users:
1341
1342 self._users[user] = {}
1343
1345 """Freeze the variable state for a user.
1346
1347 This will clone and preserve a user's entire variable state, so that it can be
1348 restored later with `thaw_uservars`."""
1349
1350 if user in self._users:
1351
1352 self._freeze[user] = copy.deepcopy(self._users[user])
1353 else:
1354 self._warn("Can't freeze vars for user " + user + ": not found!")
1355
1357 """Thaw a user's frozen variables.
1358
1359 The `action` can be one of the following options:
1360
1361 discard: Don't restore the user's variables, just delete the frozen copy.
1362 keep: Keep the frozen copy after restoring the variables.
1363 thaw: Restore the variables, then delete the frozen copy (default)."""
1364
1365 if user in self._freeze:
1366
1367 if action == "thaw":
1368
1369 self.clear_uservars(user)
1370 self._users[user] = copy.deepcopy(self._freeze[user])
1371 del self._freeze[user]
1372 elif action == "discard":
1373
1374 del self._freeze[user]
1375 elif action == "keep":
1376
1377 self.clear_uservars(user)
1378 self._users[user] = copy.deepcopy(self._freeze[user])
1379 else:
1380 self._warn("Unsupported thaw action")
1381 else:
1382 self._warn("Can't thaw vars for user " + user + ": not found!")
1383
1385 """Get the last trigger matched for the user.
1386
1387 This will return the raw trigger text that the user's last message matched. If
1388 there was no match, this will return None."""
1389 return self.get_uservar(user, "__lastmatch__")
1390
1392 """Get information about a trigger.
1393
1394 Pass in a raw trigger to find out what file name and line number it appeared at.
1395 This is useful for e.g. tracking down the location of the trigger last matched
1396 by the user via last_match(). Returns a list of matching triggers, containing
1397 their topics, filenames and line numbers. Returns None if there weren't
1398 any matches found.
1399
1400 The keys in the trigger info is as follows:
1401
1402 * category: Either 'topic' (for normal) or 'thats' (for %Previous triggers)
1403 * topic: The topic name
1404 * trigger: The raw trigger text
1405 * filename: The filename the trigger was found in.
1406 * lineno: The line number the trigger was found on.
1407
1408 Pass in a true value for `dump`, and the entire syntax tracking
1409 tree is returned."""
1410 if dump:
1411 return self._syntax
1412
1413 response = None
1414
1415
1416 for category in self._syntax:
1417 for topic in self._syntax[category]:
1418 if trigger in self._syntax[category][topic]:
1419
1420 if response is None:
1421 response = list()
1422 fname, lineno = self._syntax[category][topic][trigger]['trigger']
1423 response.append(dict(
1424 category=category,
1425 topic=topic,
1426 trigger=trigger,
1427 filename=fname,
1428 line=lineno,
1429 ))
1430
1431 return response
1432
1434 """Retrieve the user ID of the current user talking to your bot.
1435
1436 This is mostly useful inside of a Python object macro to get the user ID of the
1437 person who caused the object macro to be invoked (i.e. to set a variable for
1438 that user from within the object).
1439
1440 This will return None if used outside of the context of getting a reply (i.e.
1441 the value is unset at the end of the `reply()` method)."""
1442 if self._current_user is None:
1443
1444 self._warn("current_user() is meant to be used from within a Python object macro!")
1445 return self._current_user
1446
1447
1448
1449
1450
1451 - def reply(self, user, msg):
1452 """Fetch a reply from the RiveScript brain."""
1453 self._say("Get reply to [" + user + "] " + msg)
1454
1455
1456 self._current_user = user
1457
1458
1459 msg = self._format_message(msg)
1460
1461 reply = ''
1462
1463
1464 if "__begin__" in self._topics:
1465 begin = self._getreply(user, 'request', context='begin')
1466
1467
1468 if '{ok}' in begin:
1469 reply = self._getreply(user, msg)
1470 begin = re.sub('{ok}', reply, begin)
1471
1472 reply = begin
1473
1474
1475 reply = self._process_tags(user, msg, reply)
1476 else:
1477
1478 reply = self._getreply(user, msg)
1479
1480
1481 oldInput = self._users[user]['__history__']['input'][:8]
1482 self._users[user]['__history__']['input'] = [msg]
1483 self._users[user]['__history__']['input'].extend(oldInput)
1484 oldReply = self._users[user]['__history__']['reply'][:8]
1485 self._users[user]['__history__']['reply'] = [reply]
1486 self._users[user]['__history__']['reply'].extend(oldReply)
1487
1488
1489 self._current_user = None
1490
1491 return reply
1492
1519
1520 - def _getreply(self, user, msg, context='normal', step=0):
1521
1522 if not 'topics' in self._sorted:
1523 raise Exception("You forgot to call sort_replies()!")
1524
1525
1526 if not user in self._users:
1527 self._users[user] = {'topic': 'random'}
1528
1529
1530 topic = self._users[user]['topic']
1531 stars = []
1532 thatstars = []
1533 reply = ''
1534
1535
1536 if not topic in self._topics:
1537 self._warn("User " + user + " was in an empty topic named '" + topic + "'")
1538 topic = self._users[user]['topic'] = 'random'
1539
1540
1541 if step > self._depth:
1542 return "ERR: Deep Recursion Detected"
1543
1544
1545 if context == 'begin':
1546 topic = '__begin__'
1547
1548
1549 if not '__history__' in self._users[user]:
1550 self._users[user]['__history__'] = {
1551 'input': [
1552 'undefined', 'undefined', 'undefined', 'undefined',
1553 'undefined', 'undefined', 'undefined', 'undefined',
1554 'undefined'
1555 ],
1556 'reply': [
1557 'undefined', 'undefined', 'undefined', 'undefined',
1558 'undefined', 'undefined', 'undefined', 'undefined',
1559 'undefined'
1560 ]
1561 }
1562
1563
1564 if not topic in self._topics:
1565
1566
1567 return "[ERR: No default topic 'random' was found!]"
1568
1569
1570 matched = None
1571 matchedTrigger = None
1572 foundMatch = False
1573
1574
1575
1576
1577
1578
1579 if step == 0:
1580 allTopics = [topic]
1581 if topic in self._includes or topic in self._lineage:
1582
1583 allTopics = self._get_topic_tree(topic)
1584
1585
1586 for top in allTopics:
1587 self._say("Checking topic " + top + " for any %Previous's.")
1588 if top in self._sorted["thats"]:
1589 self._say("There is a %Previous in this topic!")
1590
1591
1592 lastReply = self._users[user]["__history__"]["reply"][0]
1593
1594
1595 lastReply = self._format_message(lastReply, botreply=True)
1596
1597 self._say("lastReply: " + lastReply)
1598
1599
1600 for trig in self._sorted["thats"][top]:
1601 botside = self._reply_regexp(user, trig)
1602 self._say("Try to match lastReply (" + lastReply + ") to " + botside)
1603
1604
1605 match = re.match(r'^' + botside + r'$', lastReply)
1606 if match:
1607
1608 self._say("Bot side matched!")
1609 thatstars = match.groups()
1610 for subtrig in self._sorted["that_trig"][top][trig]:
1611 humanside = self._reply_regexp(user, subtrig)
1612 self._say("Now try to match " + msg + " to " + humanside)
1613
1614 match = re.match(r'^' + humanside + '$', msg)
1615 if match:
1616 self._say("Found a match!")
1617 matched = self._thats[top][trig][subtrig]
1618 matchedTrigger = subtrig
1619 foundMatch = True
1620
1621
1622 stars = match.groups()
1623 break
1624
1625
1626 if foundMatch:
1627 break
1628
1629 if foundMatch:
1630 break
1631
1632
1633 if not foundMatch:
1634 for trig in self._sorted["topics"][topic]:
1635
1636 regexp = self._reply_regexp(user, trig)
1637 self._say("Try to match %r against %r (%r)" % (msg, trig, regexp))
1638
1639
1640
1641 isAtomic = self._is_atomic(trig)
1642 isMatch = False
1643 if isAtomic:
1644
1645
1646 if msg == regexp:
1647 isMatch = True
1648 else:
1649
1650 match = re.match(r'^' + regexp + r'$', msg)
1651 if match:
1652
1653 isMatch = True
1654
1655
1656 stars = match.groups()
1657
1658 if isMatch:
1659 self._say("Found a match!")
1660
1661
1662
1663 if not trig in self._topics[topic]:
1664
1665 matched = self._find_trigger_by_inheritence(topic, trig)
1666 else:
1667
1668 matched = self._topics[topic][trig]
1669
1670 foundMatch = True
1671 matchedTrigger = trig
1672 break
1673
1674
1675
1676 self._users[user]["__lastmatch__"] = matchedTrigger
1677
1678 if matched:
1679 for nil in [1]:
1680
1681 if matched["redirect"]:
1682 self._say("Redirecting us to " + matched["redirect"])
1683 redirect = self._process_tags(user, msg, matched["redirect"], stars, thatstars, step)
1684 self._say("Pretend user said: " + redirect)
1685 reply = self._getreply(user, redirect, step=(step + 1))
1686 break
1687
1688
1689 for con in sorted(matched["condition"]):
1690 halves = re.split(r'\s*=>\s*', matched["condition"][con])
1691 if halves and len(halves) == 2:
1692 condition = re.match(r'^(.+?)\s+(==|eq|!=|ne|<>|<|<=|>|>=)\s+(.+?)$', halves[0])
1693 if condition:
1694 left = condition.group(1)
1695 eq = condition.group(2)
1696 right = condition.group(3)
1697 potreply = halves[1]
1698 self._say("Left: " + left + "; eq: " + eq + "; right: " + right + " => " + potreply)
1699
1700
1701 left = self._process_tags(user, msg, left, stars, thatstars, step)
1702 right = self._process_tags(user, msg, right, stars, thatstars, step)
1703
1704
1705 if len(left) == 0:
1706 left = 'undefined'
1707 if len(right) == 0:
1708 right = 'undefined'
1709
1710 self._say("Check if " + left + " " + eq + " " + right)
1711
1712
1713 passed = False
1714 if eq == 'eq' or eq == '==':
1715 if left == right:
1716 passed = True
1717 elif eq == 'ne' or eq == '!=' or eq == '<>':
1718 if left != right:
1719 passed = True
1720 else:
1721
1722 try:
1723 left, right = int(left), int(right)
1724 if eq == '<':
1725 if left < right:
1726 passed = True
1727 elif eq == '<=':
1728 if left <= right:
1729 passed = True
1730 elif eq == '>':
1731 if left > right:
1732 passed = True
1733 elif eq == '>=':
1734 if left >= right:
1735 passed = True
1736 except:
1737 self._warn("Failed to evaluate numeric condition!")
1738
1739
1740 if passed:
1741 reply = potreply
1742 break
1743
1744
1745 if len(reply) > 0:
1746 break
1747
1748
1749 bucket = []
1750 for rep in sorted(matched["reply"]):
1751 text = matched["reply"][rep]
1752 weight = 1
1753 match = re.match(re_weight, text)
1754 if match:
1755 weight = int(match.group(1))
1756 if weight <= 0:
1757 self._warn("Can't have a weight <= 0!")
1758 weight = 1
1759 for i in range(0, weight):
1760 bucket.append(text)
1761
1762
1763 reply = random.choice(bucket)
1764 break
1765
1766
1767 if not foundMatch:
1768 reply = RS_ERR_MATCH
1769 elif len(reply) == 0:
1770 reply = RS_ERR_FOUND
1771
1772 self._say("Reply: " + reply)
1773
1774
1775 if context == "begin":
1776
1777
1778 reTopic = re.findall(r'\{topic=(.+?)\}', reply)
1779 for match in reTopic:
1780 self._say("Setting user's topic to " + match)
1781 self._users[user]["topic"] = match
1782 reply = re.sub(r'\{topic=' + re.escape(match) + r'\}', '', reply)
1783
1784 reSet = re.findall('<set (.+?)=(.+?)>', reply)
1785 for match in reSet:
1786 self._say("Set uservar " + str(match[0]) + "=" + str(match[1]))
1787 self._users[user][match[0]] = match[1]
1788 reply = re.sub('<set ' + re.escape(match[0]) + '=' + re.escape(match[1]) + '>', '', reply)
1789 else:
1790
1791 reply = self._process_tags(user, msg, reply, stars, thatstars, step)
1792
1793 return reply
1794
1796 """Run a kind of substitution on a message."""
1797
1798
1799 if not 'lists' in self._sorted:
1800 raise Exception("You forgot to call sort_replies()!")
1801 if not list in self._sorted["lists"]:
1802 raise Exception("You forgot to call sort_replies()!")
1803
1804
1805 subs = None
1806 if list == 'subs':
1807 subs = self._subs
1808 else:
1809 subs = self._person
1810
1811
1812 ph = []
1813 i = 0
1814
1815 for pattern in self._sorted["lists"][list]:
1816 result = subs[pattern]
1817
1818
1819 ph.append(result)
1820 placeholder = "\x00%d\x00" % i
1821 i += 1
1822
1823 qm = re.escape(pattern)
1824 msg = re.sub(r'^' + qm + "$", placeholder, msg)
1825 msg = re.sub(r'^' + qm + r'(\W+)', placeholder + r'\1', msg)
1826 msg = re.sub(r'(\W+)' + qm + r'(\W+)', r'\1' + placeholder + r'\2', msg)
1827 msg = re.sub(r'(\W+)' + qm + r'$', r'\1' + placeholder, msg)
1828
1829 placeholders = re.findall(r'\x00(\d+)\x00', msg)
1830 for match in placeholders:
1831 i = int(match)
1832 result = ph[i]
1833 msg = re.sub(r'\x00' + match + r'\x00', result, msg)
1834
1835
1836 return msg.strip()
1837
1839 """Prepares a trigger for the regular expression engine."""
1840
1841
1842
1843 regexp = re.sub(r'^\*$', r'<zerowidthstar>', regexp)
1844
1845
1846 regexp = re.sub(r'\*', r'(.+?)', regexp)
1847 regexp = re.sub(r'#', r'(\d+?)', regexp)
1848 regexp = re.sub(r'_', r'(\w+?)', regexp)
1849 regexp = re.sub(r'\{weight=\d+\}', '', regexp)
1850 regexp = re.sub(r'<zerowidthstar>', r'(.*?)', regexp)
1851
1852
1853 optionals = re.findall(r'\[(.+?)\]', regexp)
1854 for match in optionals:
1855 parts = match.split("|")
1856 new = []
1857 for p in parts:
1858 p = r'\s*' + p + r'\s*'
1859 new.append(p)
1860 new.append(r'\s*')
1861
1862
1863
1864 pipes = '|'.join(new)
1865 pipes = re.sub(re.escape('(.+?)'), '(?:.+?)', pipes)
1866 pipes = re.sub(re.escape('(\d+?)'), '(?:\d+?)', pipes)
1867 pipes = re.sub(re.escape('([A-Za-z]+?)'), '(?:[A-Za-z]+?)', pipes)
1868
1869 regexp = re.sub(r'\s*\[' + re.escape(match) + '\]\s*', '(?:' + pipes + ')', regexp)
1870
1871
1872 regexp = re.sub(r'\\w', r'[A-Za-z]', regexp)
1873
1874
1875 arrays = re.findall(r'\@(.+?)\b', regexp)
1876 for array in arrays:
1877 rep = ''
1878 if array in self._arrays:
1879 rep = r'(?:' + '|'.join(self._arrays[array]) + ')'
1880 regexp = re.sub(r'\@' + re.escape(array) + r'\b', rep, regexp)
1881
1882
1883 bvars = re.findall(r'<bot (.+?)>', regexp)
1884 for var in bvars:
1885 rep = ''
1886 if var in self._bvars:
1887 rep = self._strip_nasties(self._bvars[var])
1888 regexp = re.sub(r'<bot ' + re.escape(var) + r'>', rep, regexp)
1889
1890
1891 uvars = re.findall(r'<get (.+?)>', regexp)
1892 for var in uvars:
1893 rep = ''
1894 if var in self._users[user]:
1895 rep = self._strip_nasties(self._users[user][var])
1896 regexp = re.sub(r'<get ' + re.escape(var) + r'>', rep, regexp)
1897
1898
1899
1900 if '<input' in regexp or '<reply' in regexp:
1901 for type in ['input', 'reply']:
1902 tags = re.findall(r'<' + type + r'([0-9])>', regexp)
1903 for index in tags:
1904 rep = self._format_message(self._users[user]['__history__'][type][int(index) - 1])
1905 regexp = re.sub(r'<' + type + str(index) + r'>', rep, regexp)
1906 regexp = re.sub(
1907 '<' + type + '>',
1908 self._format_message(self._users[user]['__history__'][type][0]),
1909 regexp
1910 )
1911
1912
1913 return regexp
1914
2112
2123
2124
2125
2126
2127
2128 - def _topic_triggers(self, topic, triglvl, depth=0, inheritence=0, inherited=False):
2129 """Recursively scan a topic and return a list of all triggers."""
2130
2131
2132 if depth > self._depth:
2133 self._warn("Deep recursion while scanning topic inheritence")
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150 self._say("\tCollecting trigger list for topic " + topic + "(depth="
2151 + str(depth) + "; inheritence=" + str(inheritence) + "; "
2152 + "inherited=" + str(inherited) + ")")
2153
2154
2155
2156
2157
2158
2159 triggers = []
2160
2161
2162 inThisTopic = []
2163 if topic in triglvl:
2164 for trigger in triglvl[topic]:
2165 inThisTopic.append(trigger)
2166
2167
2168 if topic in self._includes:
2169
2170 for includes in self._includes[topic]:
2171 self._say("\t\tTopic " + topic + " includes " + includes)
2172 triggers.extend(self._topic_triggers(includes, triglvl, (depth + 1), inheritence, True))
2173
2174
2175 if topic in self._lineage:
2176
2177 for inherits in self._lineage[topic]:
2178 self._say("\t\tTopic " + topic + " inherits " + inherits)
2179 triggers.extend(self._topic_triggers(inherits, triglvl, (depth + 1), (inheritence + 1), False))
2180
2181
2182
2183
2184
2185 if topic in self._lineage or inherited:
2186 for trigger in inThisTopic:
2187 self._say("\t\tPrefixing trigger with {inherits=" + str(inheritence) + "}" + trigger)
2188 triggers.append("{inherits=" + str(inheritence) + "}" + trigger)
2189 else:
2190 triggers.extend(inThisTopic)
2191
2192 return triggers
2193
2195 """Locate the replies for a trigger in an inherited/included topic."""
2196
2197
2198
2199
2200
2201
2202 if depth > self._depth:
2203 self._warn("Deep recursion detected while following an inheritence trail!")
2204 return None
2205
2206
2207
2208 if topic in self._lineage:
2209 for inherits in sorted(self._lineage[topic]):
2210
2211 if trig in self._topics[inherits]:
2212
2213 return self._topics[inherits][trig]
2214 else:
2215
2216 match = self._find_trigger_by_inheritence(
2217 inherits, trig, (depth + 1)
2218 )
2219 if match:
2220
2221 return match
2222
2223
2224 if topic in self._includes:
2225 for includes in sorted(self._includes[topic]):
2226
2227 if trig in self._topics[includes]:
2228
2229 return self._topics[includes][trig]
2230 else:
2231
2232 match = self._find_trigger_by_inheritence(
2233 includes, trig, (depth + 1)
2234 )
2235 if match:
2236
2237 return match
2238
2239
2240 return None
2241
2243 """Given one topic, get the list of all included/inherited topics."""
2244
2245
2246 if depth > self._depth:
2247 self._warn("Deep recursion while scanning topic trees!")
2248 return []
2249
2250
2251 topics = [topic]
2252
2253
2254 if topic in self._includes:
2255
2256 for includes in sorted(self._includes[topic]):
2257 topics.extend(self._get_topic_tree(includes, depth + 1))
2258
2259
2260 if topic in self._lineage:
2261
2262 for inherits in sorted(self._lineage[topic]):
2263 topics.extend(self._get_topic_tree(inherits, depth + 1))
2264
2265 return topics
2266
2267
2268
2269
2270
2272 """Determine if a trigger is atomic or not."""
2273
2274
2275
2276
2277 special = ['*', '#', '_', '(', '[', '<']
2278 for char in special:
2279 if char in trigger:
2280 return False
2281
2282 return True
2283
2285 """Count the words that aren't wildcards in a trigger."""
2286 words = []
2287 if all:
2288 words = re.split(re_ws, trigger)
2289 else:
2290 words = re.split(re_wilds, trigger)
2291
2292 wc = 0
2293 for word in words:
2294 if len(word) > 0:
2295 wc += 1
2296
2297 return wc
2298
2300 """Formats a string for ASCII regex matching."""
2301 s = re.sub(re_nasties, '', s)
2302 return s
2303
2305 """For debugging, dump the entire data structure."""
2306 pp = pprint.PrettyPrinter(indent=4)
2307
2308 print("=== Variables ===")
2309 print("-- Globals --")
2310 pp.pprint(self._gvars)
2311 print("-- Bot vars --")
2312 pp.pprint(self._bvars)
2313 print("-- Substitutions --")
2314 pp.pprint(self._subs)
2315 print("-- Person Substitutions --")
2316 pp.pprint(self._person)
2317 print("-- Arrays --")
2318 pp.pprint(self._arrays)
2319
2320 print("=== Topic Structure ===")
2321 pp.pprint(self._topics)
2322 print("=== %Previous Structure ===")
2323 pp.pprint(self._thats)
2324
2325 print("=== Includes ===")
2326 pp.pprint(self._includes)
2327
2328 print("=== Inherits ===")
2329 pp.pprint(self._lineage)
2330
2331 print("=== Sort Buffer ===")
2332 pp.pprint(self._sorted)
2333
2334 print("=== Syntax Tree ===")
2335 pp.pprint(self._syntax)
2336
2337
2338
2339
2340
2341 if __name__ == "__main__":
2342 from interactive import interactive_mode
2343 interactive_mode()
2344
2345
2346