Package rivescript :: Module rivescript
[hide private]
[frames] | no frames]

Source Code for Module rivescript.rivescript

   1  #!/usr/bin/env python 
   2   
   3  # The MIT License (MIT) 
   4  # 
   5  # Copyright (c) 2014 Noah Petherbridge 
   6  # 
   7  # Permission is hereby granted, free of charge, to any person obtaining a copy 
   8  # of this software and associated documentation files (the "Software"), to deal 
   9  # in the Software without restriction, including without limitation the rights 
  10  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
  11  # copies of the Software, and to permit persons to whom the Software is 
  12  # furnished to do so, subject to the following conditions: 
  13  # 
  14  # The above copyright notice and this permission notice shall be included in all 
  15  # copies or substantial portions of the Software. 
  16  # 
  17  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
  18  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
  19  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
  20  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
  21  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
  22  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
  23  # SOFTWARE. 
  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  # Common regular expressions. 
  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  # Version of RiveScript we support. 
  47  rs_version = 2.0 
  48   
  49  # Exportable constants. 
  50  RS_ERR_MATCH = "ERR: No Reply Matched" 
  51  RS_ERR_REPLY = "ERR: No Reply Found" 
52 53 54 -class RiveScript:
55 """A RiveScript interpreter for Python 2 and 3.""" 56 57 ############################################################################ 58 # Initialization and Utility Methods # 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 # Instance variables. 70 self._debug = debug # Debug mode 71 self._log = log # Debug log file 72 self._utf8 = utf8 # UTF-8 mode 73 self._strict = strict # Strict mode 74 self._depth = depth # Recursion depth limit 75 self._gvars = {} # 'global' variables 76 self._bvars = {} # 'bot' variables 77 self._subs = {} # 'sub' variables 78 self._person = {} # 'person' variables 79 self._arrays = {} # 'array' variables 80 self._users = {} # 'user' variables 81 self._freeze = {} # frozen 'user' variables 82 self._includes = {} # included topics 83 self._lineage = {} # inherited topics 84 self._handlers = {} # Object handlers 85 self._objlangs = {} # Languages of objects used 86 self._topics = {} # Main reply structure 87 self._thats = {} # %Previous reply structure 88 self._sorted = {} # Sorted buffers 89 self._syntax = {} # Syntax tracking (filenames & line no.'s) 90 91 # "Current request" variables. 92 self._current_user = None # The current user ID. 93 94 # Define the default Python language handler. 95 self._handlers["python"] = python.PyRiveObjects() 96 97 self._say("Interpreter initialized.")
98 99 @classmethod
100 - def VERSION(self=None):
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 # Log it to the file. 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 # Loading and Parsing Methods # 126 ############################################################################ 127
128 - def load_directory(self, directory, ext=None):
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 # Use the default extensions - .rive is preferable. 137 ext = ['.rive', '.rs'] 138 elif type(ext) == str: 139 # Backwards compatibility for ext being a string value. 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 # Load this file. 150 self.load_file(os.path.join(directory, item)) 151 break
152
153 - def load_file(self, filename):
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
164 - def stream(self, code):
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 # Track temporary variables. 176 topic = 'random' # Default topic=random 177 lineno = 0 # Line numbers for syntax tracking 178 comment = False # In a multi-line comment 179 inobj = False # In an object 180 objname = '' # The name of the object we're in 181 objlang = '' # The programming language of the object 182 objbuf = [] # Object contents buffer 183 ontrig = '' # The current trigger 184 repcnt = 0 # Reply counter 185 concnt = 0 # Condition counter 186 lastcmd = '' # Last command code 187 isThat = '' # Is a %Previous trigger 188 189 # Read each line. 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: # Skip blank lines 195 continue 196 197 # In an object? 198 if inobj: 199 if re.match(re_objend, line): 200 # End the object. 201 if len(objname): 202 # Call the object's handler. 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() # Trim excess space. We do it down here so we 217 # don't mess up python objects! 218 219 # Look for comments. 220 if line[:2] == '//': # A single-line comment. 221 continue 222 elif line[0] == '#': 223 self._warn("Using the # symbol for comments is deprecated", fname, lineno) 224 elif line[:2] == '/*': # Start of a multi-line comment. 225 if not '*/' in line: # Cancel if the end is here too. 226 comment = True 227 continue 228 elif '*/' in line: 229 comment = False 230 continue 231 if comment: 232 continue 233 234 # Separate the command from the data. 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 # Ignore inline comments if there's a space before and after 242 # the // symbols. 243 if " // " in line: 244 line = line.split(" // ")[0].strip() 245 246 # Run a syntax check on this line. 247 syntax_error = self.check_syntax(cmd, line) 248 if syntax_error: 249 # There was a syntax error! Are we enforcing strict mode? 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 # Don't try to continue 257 258 # Reset the %Previous state if this is a new +Trigger. 259 if cmd == '+': 260 isThat = '' 261 262 # Do a lookahead for ^Continue and %Previous commands. 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 # Only continue if the lookahead line has any data. 271 if len(lookahead) != 0: 272 # The lookahead command has to be either a % or a ^. 273 if lookCmd != '^' and lookCmd != '%': 274 break 275 276 # If the current command is a +, see if the following is 277 # a %. 278 if cmd == '+': 279 if lookCmd == '%': 280 isThat = lookahead 281 break 282 else: 283 isThat = '' 284 285 # If the current command is a ! and the next command(s) are 286 # ^, we'll tack each extension on as a line break (which is 287 # useful information for arrays). 288 if cmd == '!': 289 if lookCmd == '^': 290 line += "<crlf>" + lookahead 291 continue 292 293 # If the current command is not a ^ and the line after is 294 # not a %, but the line after IS a ^, then tack it on to the 295 # end of the current line. 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 # Handle the types of RiveScript commands. 305 if cmd == '!': 306 # ! DEFINE 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 # Remove 'fake' line breaks unless this is an array. 318 if type != 'array': 319 value = re.sub(r'<crlf>', '', value) 320 321 # Handle version numbers. 322 if type == 'version': 323 # Verify we support it. 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 # All other types of defines require a variable and value name. 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 # Handle the rest of the types. 341 if type == 'global': 342 # 'Global' variables 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 # Handle flipping debug and depth vars. 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 # Bot variables 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 # Arrays 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 # Did this have multiple parts? 393 parts = value.split("<crlf>") 394 395 # Process each line of array data. 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 # Convert any remaining '\s' escape codes into spaces. 404 for f in fields: 405 f = f.replace(r'\s', ' ') 406 407 self._arrays[var] = fields 408 elif type == 'sub': 409 # Substitutions 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 # Person Substitutions 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 # > LABEL 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 # Handle the label types. 444 if type == 'begin': 445 # The BEGIN block. 446 self._say("\tFound the BEGIN block.") 447 type = 'topic' 448 name = '__begin__' 449 if type == 'topic': 450 # Starting a new topic. 451 self._say("\tSet topic to " + name) 452 ontrig = '' 453 topic = name 454 455 # Does this topic include or inherit another one? 456 mode = '' # or 'inherits' or 'includes' 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 # This topic is either inherited or included. 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 # If a field was provided, it should be the programming 475 # language. 476 lang = None 477 if len(fields) > 0: 478 lang = fields[0].lower() 479 480 # Only try to parse a language we support. 481 ontrig = '' 482 if lang is None: 483 self._warn("Trying to parse unknown programming language", fname, lineno) 484 lang = 'python' # Assume it's Python. 485 486 # See if we have a defined handler for this language. 487 if lang in self._handlers: 488 # We have a handler, so start loading the code. 489 objname = name 490 objlang = lang 491 objbuf = [] 492 inobj = True 493 else: 494 # We don't have a handler, just ignore it. 495 objname = '' 496 objlang = '' 497 objbuf = [] 498 inobj = True 499 else: 500 self._warn("Unknown label type '" + type + "'", fname, lineno) 501 elif cmd == '<': 502 # < LABEL 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 # + TRIGGER 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 # - REPLY 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 # % PREVIOUS 540 pass # This was handled above. 541 elif cmd == '^': 542 # ^ CONTINUE 543 pass # This was handled above. 544 elif cmd == '@': 545 # @ REDIRECT 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 # * CONDITION 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
567 - def check_syntax(self, cmd, line):
568 """Syntax check a RiveScript command and line. 569 570 Returns a syntax error string on error; None otherwise.""" 571 572 # Run syntax checks based on the type of command. 573 if cmd == '!': 574 # ! Definition 575 # - Must be formatted like this: 576 # ! type name = value 577 # OR 578 # ! type = value 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 # > Label 584 # - The "begin" label must have only one argument ("begin") 585 # - "topic" labels must be lowercased but can inherit other topics (a-z0-9_\s) 586 # - "object" labels must follow the same rules as "topic", but don't need to be lowercase 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 # + Trigger, % Previous, @ Redirect 602 # This one is strict. The triggers are to be run through the regexp engine, 603 # therefore it should be acceptable for the regexp engine. 604 # - Entirely lowercase 605 # - No symbols except: ( | ) [ ] * _ # @ { } < > = 606 # - All brackets should be matched 607 parens = 0 # Open parenthesis 608 square = 0 # Open square brackets 609 curly = 0 # Open curly brackets 610 angle = 0 # Open angled brackets 611 612 # Count brackets. 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 # Any mismatches? 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 # In UTF-8 mode, most symbols are allowed. 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 # - Trigger, ^ Continue, / Comment 652 # These commands take verbatim arguments, so their syntax is loose. 653 pass 654 elif cmd == '*': 655 # * Condition 656 # Syntax for a conditional is as follows: 657 # * value symbol value => response 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
664 - def deparse(self):
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 # Data to return. 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 # Populate the config fields. 689 if self._debug: 690 result["begin"]["global"]["debug"] = self._debug 691 if self._depth != 50: 692 result["begin"]["global"]["depth"] = 50 693 694 # Definitions 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 # Topic Triggers. 702 for topic in self._topics: 703 dest = {} # Where to place the topic info 704 705 if topic == "__begin__": 706 # Begin block. 707 dest = result["begin"]["triggers"] 708 else: 709 # Normal topic. 710 if not topic in result["topic"]: 711 result["topic"][topic] = {} 712 dest = result["topic"][topic] 713 714 # Copy the triggers. 715 for trig, data in self._topics[topic].iteritems(): 716 dest[trig] = self._copy_trigger(trig, data) 717 718 # %Previous's. 719 for topic in self._thats: 720 dest = {} # Where to place the topic info 721 722 if topic == "__begin__": 723 # Begin block. 724 dest = result["begin"]["that"] 725 else: 726 # Normal topic. 727 if not topic in result["that"]: 728 result["that"][topic] = {} 729 dest = result["that"][topic] 730 731 # The "that" structure is backwards: bot reply, then trigger, then info. 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 # Inherits/Includes. 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 # Passed a string instead of a file handle? 762 if type(fh) is str: 763 fh = codecs.open(fh, "w", "utf-8") 764 765 # Deparse the loaded data. 766 if deparsed is None: 767 deparsed = self.deparse() 768 769 # Start at the beginning. 770 fh.write("// Written by rivescript.deparse()\n") 771 fh.write("! version = 2.0\n\n") 772 773 # Variables of all sorts! 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 # Array types need to be separated by either spaces or pipes. 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 # Word-wrap the result, target width is 78 chars minus the 789 # kind, var, and spaces and equals sign. 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 # Begin block. 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 # The topics. Random first! 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 # Used > topic tag 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 # Any %Previous's? 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
844 - def _copy_trigger(self, trig, data, previous=None):
845 """Make copies of all data below a trigger.""" 846 # Copied data. 847 dest = {} 848 849 if previous: 850 dest["previous"] = previous 851 852 if "redirect" in data and data["redirect"]: 853 # @Redirect 854 dest["redirect"] = data["redirect"] 855 856 if "condition" in data and len(data["condition"].keys()): 857 # *Condition 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 # -Reply 864 dest["reply"] = [] 865 for i in sorted(data["reply"].keys()): 866 dest["reply"].append(data["reply"][i]) 867 868 return dest
869
870 - def _write_triggers(self, fh, triggers, indent=""):
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
893 - def _write_wrapped(self, line, sep=" ", indent="", width=78):
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 # Need to word wrap! 906 words.insert(0, buf.pop()) # Undo 907 lines.append(sep.join(buf)) 908 buf = [] 909 line = "" 910 911 # Straggler? 912 if line: 913 lines.append(line) 914 915 # Returned output 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 # Sorting Methods # 959 ############################################################################ 960
961 - def sort_replies(self, thats=False):
962 """Sort the loaded triggers.""" 963 # This method can sort both triggers and that's. 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 # (Re)Initialize the sort cache. 974 self._sorted[sortlvl] = {} 975 976 self._say("Sorting triggers...") 977 978 # Loop through all the topics. 979 for topic in triglvl: 980 self._say("Analyzing topic " + topic) 981 982 # Collect a list of all the triggers we're going to need to worry 983 # about. If this topic inherits another topic, we need to 984 # recursively add those to the list. 985 alltrig = self._topic_triggers(topic, triglvl) 986 987 # Keep in mind here that there is a difference between 'includes' 988 # and 'inherits' -- topics that inherit other topics are able to 989 # OVERRIDE triggers that appear in the inherited topic. This means 990 # that if the top topic has a trigger of simply '*', then *NO* 991 # triggers are capable of matching in ANY inherited topic, because 992 # even though * has the lowest sorting priority, it has an automatic 993 # priority over all inherited topics. 994 # 995 # The _topic_triggers method takes this into account. All topics 996 # that inherit other topics will have their triggers prefixed with 997 # a fictional {inherits} tag, which would start at {inherits=0} and 998 # increment if the topic tree has other inheriting topics. So we can 999 # use this tag to make sure topics that inherit things will have 1000 # their triggers always be on top of the stack, from inherits=0 to 1001 # inherits=n. 1002 1003 # Sort these triggers. 1004 running = self._sort_trigger_set(alltrig) 1005 1006 # Save this topic's sorted list. 1007 if not sortlvl in self._sorted: 1008 self._sorted[sortlvl] = {} 1009 self._sorted[sortlvl][topic] = running 1010 1011 # And do it all again for %Previous! 1012 if not thats: 1013 # This will sort the %Previous lines to best match the bot's last reply. 1014 self.sort_replies(True) 1015 1016 # If any of those %Previous's had more than one +trigger for them, 1017 # this will sort all those +triggers to pair back the best human 1018 # interaction. 1019 self._sort_that_triggers() 1020 1021 # Also sort both kinds of substitutions. 1022 self._sort_list('subs', self._subs) 1023 self._sort_list('person', self._person)
1024
1025 - def _sort_that_triggers(self):
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
1042 - def _sort_trigger_set(self, triggers):
1043 """Sort a group of triggers in optimal sorting order.""" 1044 1045 # Create a priority map. 1046 prior = { 1047 0: [] # Default priority=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 # Keep a running list of sorted triggers for this topic. 1060 running = [] 1061 1062 # Sort them by priority. 1063 for p in sorted(prior.keys(), reverse=True): 1064 self._say("\tSorting triggers with priority " + str(p)) 1065 1066 # So, some of these triggers may include {inherits} tags, if they 1067 # came form a topic which inherits another topic. Lower inherits 1068 # values mean higher priority on the stack. 1069 inherits = -1 # -1 means no {inherits} tag 1070 highest_inherits = -1 # highest inheritence number seen 1071 1072 # Loop through and categorize these triggers. 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 # See if it has an inherits tag. 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 # If this is the first time we've seen this inheritence level, 1092 # initialize its track structure. 1093 if not inherits in track: 1094 track[inherits] = self._init_sort_track() 1095 1096 # Start inspecting the trigger's contents. 1097 if '_' in trig: 1098 # Alphabetic wildcard included. 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 # Numeric wildcard included. 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 # Wildcard included. 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 # Optionals included. 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 # Totally atomic. 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 # Move the no-{inherits} triggers to the bottom of the stack. 1143 track[highest_inherits + 1] = track[-1] 1144 del(track[-1]) 1145 1146 # Add this group to the sort list. 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
1157 - def _sort_list(self, name, items):
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 # Initialize the list sort buffer. 1164 if not "lists" in self._sorted: 1165 self._sorted["lists"] = {} 1166 self._sorted["lists"][name] = [] 1167 1168 # Track by number of words. 1169 track = {} 1170 1171 # Loop through each item. 1172 for item in items: 1173 # Count the words. 1174 cword = self._word_count(item, all=True) 1175 if not cword in track: 1176 track[cword] = [] 1177 track[cword].append(item) 1178 1179 # Sort them. 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
1187 - def _init_sort_track(self):
1188 """Returns a new dict for keeping track of triggers for sorting.""" 1189 return { 1190 'atomic': {}, # Sort by number of whole words 1191 'option': {}, # Sort optionals by number of words 1192 'alpha': {}, # Sort alpha wildcards by no. of words 1193 'number': {}, # Sort number wildcards by no. of words 1194 'wild': {}, # Sort wildcards by no. of words 1195 'pound': [], # Triggers of just # 1196 'under': [], # Triggers of just _ 1197 'star': [] # Triggers of just * 1198 }
1199 1200 1201 ############################################################################ 1202 # Public Configuration Methods # 1203 ############################################################################ 1204
1205 - def set_handler(self, language, obj):
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 # Allow them to delete a handler too. 1231 if obj is None: 1232 if language in self._handlers: 1233 del self._handlers[language] 1234 else: 1235 self._handlers[language] = obj
1236
1237 - def set_subroutine(self, name, code):
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 # Do we have a Python handler? 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
1253 - def set_global(self, name, value):
1254 """Set a global variable. 1255 1256 Equivalent to `! global` in RiveScript code. Set to None to delete.""" 1257 if value is None: 1258 # Unset the variable. 1259 if name in self._gvars: 1260 del self._gvars[name] 1261 self._gvars[name] = value
1262
1263 - def set_variable(self, name, value):
1264 """Set a bot variable. 1265 1266 Equivalent to `! var` in RiveScript code. Set to None to delete.""" 1267 if value is None: 1268 # Unset the variable. 1269 if name in self._bvars: 1270 del self._bvars[name] 1271 self._bvars[name] = value
1272
1273 - def set_substitution(self, what, rep):
1274 """Set a substitution. 1275 1276 Equivalent to `! sub` in RiveScript code. Set to None to delete.""" 1277 if rep is None: 1278 # Unset the variable. 1279 if what in self._subs: 1280 del self._subs[what] 1281 self._subs[what] = rep
1282
1283 - def set_person(self, what, rep):
1284 """Set a person substitution. 1285 1286 Equivalent to `! person` in RiveScript code. Set to None to delete.""" 1287 if rep is None: 1288 # Unset the variable. 1289 if what in self._person: 1290 del self._person[what] 1291 self._person[what] = rep
1292
1293 - def set_uservar(self, user, name, value):
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
1301 - def get_uservar(self, user, name):
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
1315 - def get_uservars(self, user=None):
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 # All the users! 1323 return self._users 1324 elif user in self._users: 1325 # Just this one! 1326 return self._users[user] 1327 else: 1328 # No info. 1329 return None
1330
1331 - def clear_uservars(self, user=None):
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 # All the users! 1339 self._users = {} 1340 elif user in self._users: 1341 # Just this one. 1342 self._users[user] = {}
1343
1344 - def freeze_uservars(self, user):
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 # Clone the user's data. 1352 self._freeze[user] = copy.deepcopy(self._users[user]) 1353 else: 1354 self._warn("Can't freeze vars for user " + user + ": not found!")
1355
1356 - def thaw_uservars(self, user, action="thaw"):
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 # What are we doing? 1367 if action == "thaw": 1368 # Thawing them out. 1369 self.clear_uservars(user) 1370 self._users[user] = copy.deepcopy(self._freeze[user]) 1371 del self._freeze[user] 1372 elif action == "discard": 1373 # Just discard the frozen copy. 1374 del self._freeze[user] 1375 elif action == "keep": 1376 # Keep the frozen copy afterward. 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
1384 - def last_match(self, user):
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
1391 - def trigger_info(self, trigger=None, dump=False):
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 # Search the syntax tree for the trigger. 1416 for category in self._syntax: 1417 for topic in self._syntax[category]: 1418 if trigger in self._syntax[category][topic]: 1419 # We got a match! 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
1433 - def current_user(self):
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 # They're doing it wrong. 1444 self._warn("current_user() is meant to be used from within a Python object macro!") 1445 return self._current_user
1446 1447 ############################################################################ 1448 # Reply Fetching Methods # 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 # Store the current user in case an object macro needs it. 1456 self._current_user = user 1457 1458 # Format their message. 1459 msg = self._format_message(msg) 1460 1461 reply = '' 1462 1463 # If the BEGIN block exists, consult it first. 1464 if "__begin__" in self._topics: 1465 begin = self._getreply(user, 'request', context='begin') 1466 1467 # Okay to continue? 1468 if '{ok}' in begin: 1469 reply = self._getreply(user, msg) 1470 begin = re.sub('{ok}', reply, begin) 1471 1472 reply = begin 1473 1474 # Run more tag substitutions. 1475 reply = self._process_tags(user, msg, reply) 1476 else: 1477 # Just continue then. 1478 reply = self._getreply(user, msg) 1479 1480 # Save their reply history. 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 # Unset the current user. 1489 self._current_user = None 1490 1491 return reply
1492
1493 - def _format_message(self, msg, botreply=False):
1494 """Format a user's message for safe processing.""" 1495 1496 # Make sure the string is Unicode for Python 2. 1497 if sys.version_info[0] < 3 and isinstance(msg, str): 1498 msg = msg.decode('utf8') 1499 1500 # Lowercase it. 1501 msg = msg.lower() 1502 1503 # Run substitutions on it. 1504 msg = self._substitute(msg, "subs") 1505 1506 # In UTF-8 mode, only strip metacharacters and HTML brackets 1507 # (to protect from obvious XSS attacks). 1508 if self._utf8: 1509 msg = re.sub(r'[\\<>]', '', msg) 1510 1511 # For the bot's reply, also strip common punctuation. 1512 if botreply: 1513 msg = re.sub(r'[.?,!;:@#$%^&*()]', '', msg) 1514 else: 1515 # For everything else, strip all non-alphanumerics. 1516 msg = self._strip_nasties(msg) 1517 1518 return msg
1519
1520 - def _getreply(self, user, msg, context='normal', step=0):
1521 # Needed to sort replies? 1522 if not 'topics' in self._sorted: 1523 raise Exception("You forgot to call sort_replies()!") 1524 1525 # Initialize the user's profile? 1526 if not user in self._users: 1527 self._users[user] = {'topic': 'random'} 1528 1529 # Collect data on the user. 1530 topic = self._users[user]['topic'] 1531 stars = [] 1532 thatstars = [] # For %Previous's. 1533 reply = '' 1534 1535 # Avoid letting them fall into a missing topic. 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 # Avoid deep recursion. 1541 if step > self._depth: 1542 return "ERR: Deep Recursion Detected" 1543 1544 # Are we in the BEGIN statement? 1545 if context == 'begin': 1546 topic = '__begin__' 1547 1548 # Initialize this user's history. 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 # More topic sanity checking. 1564 if not topic in self._topics: 1565 # This was handled before, which would mean topic=random and 1566 # it doesn't exist. Serious issue! 1567 return "[ERR: No default topic 'random' was found!]" 1568 1569 # Create a pointer for the matched data when we find it. 1570 matched = None 1571 matchedTrigger = None 1572 foundMatch = False 1573 1574 # See if there were any %Previous's in this topic, or any topic related 1575 # to it. This should only be done the first time -- not during a 1576 # recursive redirection. This is because in a redirection, "lastreply" 1577 # is still gonna be the same as it was the first time, causing an 1578 # infinite loop! 1579 if step == 0: 1580 allTopics = [topic] 1581 if topic in self._includes or topic in self._lineage: 1582 # Get all the topics! 1583 allTopics = self._get_topic_tree(topic) 1584 1585 # Scan them all! 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 # Do we have history yet? 1592 lastReply = self._users[user]["__history__"]["reply"][0] 1593 1594 # Format the bot's last reply the same way as the human's. 1595 lastReply = self._format_message(lastReply, botreply=True) 1596 1597 self._say("lastReply: " + lastReply) 1598 1599 # See if it's a match. 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 # Match?? 1605 match = re.match(r'^' + botside + r'$', lastReply) 1606 if match: 1607 # Huzzah! See if OUR message is right too. 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 # Get the stars! 1622 stars = match.groups() 1623 break 1624 1625 # Break if we found a match. 1626 if foundMatch: 1627 break 1628 # Break if we found a match. 1629 if foundMatch: 1630 break 1631 1632 # Search their topic for a match to their trigger. 1633 if not foundMatch: 1634 for trig in self._sorted["topics"][topic]: 1635 # Process the triggers. 1636 regexp = self._reply_regexp(user, trig) 1637 self._say("Try to match %r against %r (%r)" % (msg, trig, regexp)) 1638 1639 # Python's regular expression engine is slow. Try a verbatim 1640 # match if this is an atomic trigger. 1641 isAtomic = self._is_atomic(trig) 1642 isMatch = False 1643 if isAtomic: 1644 # Only look for exact matches, no sense running atomic triggers 1645 # through the regexp engine. 1646 if msg == regexp: 1647 isMatch = True 1648 else: 1649 # Non-atomic triggers always need the regexp. 1650 match = re.match(r'^' + regexp + r'$', msg) 1651 if match: 1652 # The regexp matched! 1653 isMatch = True 1654 1655 # Collect the stars. 1656 stars = match.groups() 1657 1658 if isMatch: 1659 self._say("Found a match!") 1660 1661 # We found a match, but what if the trigger we've matched 1662 # doesn't belong to our topic? Find it! 1663 if not trig in self._topics[topic]: 1664 # We have to find it. 1665 matched = self._find_trigger_by_inheritence(topic, trig) 1666 else: 1667 # We do have it! 1668 matched = self._topics[topic][trig] 1669 1670 foundMatch = True 1671 matchedTrigger = trig 1672 break 1673 1674 # Store what trigger they matched on. If their matched trigger is None, 1675 # this will be too, which is great. 1676 self._users[user]["__lastmatch__"] = matchedTrigger 1677 1678 if matched: 1679 for nil in [1]: 1680 # See if there are any hard redirects. 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 # Check the conditionals. 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 # Process tags all around. 1701 left = self._process_tags(user, msg, left, stars, thatstars, step) 1702 right = self._process_tags(user, msg, right, stars, thatstars, step) 1703 1704 # Defaults? 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 # Validate it. 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 # Gasp, dealing with numbers here... 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 # How truthful? 1740 if passed: 1741 reply = potreply 1742 break 1743 1744 # Have our reply yet? 1745 if len(reply) > 0: 1746 break 1747 1748 # Process weights in the replies. 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 # Get a random reply. 1763 reply = random.choice(bucket) 1764 break 1765 1766 # Still no reply? 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 # Process tags for the BEGIN block. 1775 if context == "begin": 1776 # BEGIN blocks can only set topics and uservars. The rest happen 1777 # later! 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 # Process more tags if not in BEGIN. 1791 reply = self._process_tags(user, msg, reply, stars, thatstars, step) 1792 1793 return reply
1794
1795 - def _substitute(self, msg, list):
1796 """Run a kind of substitution on a message.""" 1797 1798 # Safety checking. 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 # Get the substitution map. 1805 subs = None 1806 if list == 'subs': 1807 subs = self._subs 1808 else: 1809 subs = self._person 1810 1811 # Make placeholders each time we substitute something. 1812 ph = [] 1813 i = 0 1814 1815 for pattern in self._sorted["lists"][list]: 1816 result = subs[pattern] 1817 1818 # Make a placeholder. 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 # Strip & return. 1836 return msg.strip()
1837
1838 - def _reply_regexp(self, user, regexp):
1839 """Prepares a trigger for the regular expression engine.""" 1840 1841 # If the trigger is simply '*' then the * there needs to become (.*?) 1842 # to match the blank string too. 1843 regexp = re.sub(r'^\*$', r'<zerowidthstar>', regexp) 1844 1845 # Simple replacements. 1846 regexp = re.sub(r'\*', r'(.+?)', regexp) # Convert * into (.+?) 1847 regexp = re.sub(r'#', r'(\d+?)', regexp) # Convert # into (\d+?) 1848 regexp = re.sub(r'_', r'(\w+?)', regexp) # Convert _ into (\w+?) 1849 regexp = re.sub(r'\{weight=\d+\}', '', regexp) # Remove {weight} tags 1850 regexp = re.sub(r'<zerowidthstar>', r'(.*?)', regexp) 1851 1852 # Optionals. 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 # If this optional had a star or anything in it, make it 1863 # non-matching. 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 # _ wildcards can't match numbers! 1872 regexp = re.sub(r'\\w', r'[A-Za-z]', regexp) 1873 1874 # Filter in arrays. 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 # Filter in bot variables. 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 # Filter in user variables. 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 # Filter in <input> and <reply> tags. This is a slow process, so only 1899 # do it if we have to! 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 # TODO: the Perl version doesn't do just <input>/<reply> in trigs! 1912 1913 return regexp
1914
1915 - def _process_tags(self, user, msg, reply, st=[], bst=[], depth=0):
1916 """Post process tags in a message.""" 1917 stars = [''] 1918 stars.extend(st) 1919 botstars = [''] 1920 botstars.extend(bst) 1921 if len(stars) == 1: 1922 stars.append("undefined") 1923 if len(botstars) == 1: 1924 botstars.append("undefined") 1925 1926 # Tag shortcuts. 1927 reply = re.sub('<person>', '{person}<star>{/person}', reply) 1928 reply = re.sub('<@>', '{@<star>}', reply) 1929 reply = re.sub('<formal>', '{formal}<star>{/formal}', reply) 1930 reply = re.sub('<sentence>', '{sentence}<star>{/sentence}', reply) 1931 reply = re.sub('<uppercase>', '{uppercase}<star>{/uppercase}', reply) 1932 reply = re.sub('<lowercase>', '{lowercase}<star>{/lowercase}', reply) 1933 1934 # Weight and <star> tags. 1935 reply = re.sub(r'\{weight=\d+\}', '', reply) # Leftover {weight}s 1936 if len(stars) > 0: 1937 reply = re.sub('<star>', stars[1], reply) 1938 reStars = re.findall(r'<star(\d+)>', reply) 1939 for match in reStars: 1940 if int(match) < len(stars): 1941 reply = re.sub(r'<star' + match + '>', stars[int(match)], reply) 1942 if len(botstars) > 0: 1943 reply = re.sub('<botstar>', botstars[1], reply) 1944 reStars = re.findall(r'<botstar(\d+)>', reply) 1945 for match in reStars: 1946 if int(match) < len(botstars): 1947 reply = re.sub(r'<botstar' + match + '>', botstars[int(match)], reply) 1948 1949 # <input> and <reply> 1950 reply = re.sub('<input>', self._users[user]['__history__']['input'][0], reply) 1951 reply = re.sub('<reply>', self._users[user]['__history__']['reply'][0], reply) 1952 reInput = re.findall(r'<input([1-9])>', reply) 1953 for match in reInput: 1954 reply = re.sub(r'<input' + match + r'>', self._users[user]['__history__']['input'][int(match) - 1], reply) 1955 reReply = re.findall(r'<reply([1-9])>', reply) 1956 for match in reReply: 1957 reply = re.sub(r'<reply' + match + r'>', self._users[user]['__history__']['reply'][int(match) - 1], reply) 1958 1959 # <id> and escape codes. 1960 reply = re.sub(r'<id>', user, reply) 1961 reply = re.sub(r'\\s', ' ', reply) 1962 reply = re.sub(r'\\n', "\n", reply) 1963 reply = re.sub(r'\\#', r'#', reply) 1964 1965 # Random bits. 1966 reRandom = re.findall(r'\{random\}(.+?)\{/random\}', reply) 1967 for match in reRandom: 1968 output = '' 1969 if '|' in match: 1970 output = random.choice(match.split('|')) 1971 else: 1972 output = random.choice(match.split(' ')) 1973 reply = re.sub(r'\{random\}' + re.escape(match) + r'\{/random\}', output, reply) 1974 1975 # Person Substitutions and String Formatting. 1976 for item in ['person', 'formal', 'sentence', 'uppercase', 'lowercase']: 1977 matcher = re.findall(r'\{' + item + r'\}(.+?)\{/' + item + r'\}', reply) 1978 for match in matcher: 1979 output = None 1980 if item == 'person': 1981 # Person substitutions. 1982 output = self._substitute(match, "person") 1983 else: 1984 output = self._string_format(match, item) 1985 reply = re.sub(r'\{' + item + r'\}' + re.escape(match) + '\{/' + item + r'\}', output, reply) 1986 1987 # Bot variables: set (TODO: Perl RS doesn't support this) 1988 reBotSet = re.findall(r'<bot (.+?)=(.+?)>', reply) 1989 for match in reBotSet: 1990 self._say("Set bot variable " + str(match[0]) + "=" + str(match[1])) 1991 self._bvars[match[0]] = match[1] 1992 reply = re.sub(r'<bot ' + re.escape(match[0]) + '=' + re.escape(match[1]) + '>', '', reply) 1993 1994 # Bot variables: get 1995 reBot = re.findall(r'<bot (.+?)>', reply) 1996 for match in reBot: 1997 val = 'undefined' 1998 if match in self._bvars: 1999 val = self._bvars[match] 2000 reply = re.sub(r'<bot ' + re.escape(match) + '>', val, reply) 2001 2002 # Global vars: set (TODO: Perl RS doesn't support this) 2003 reEnvSet = re.findall(r'<env (.+?)=(.+?)>', reply) 2004 for match in reEnvSet: 2005 self._say("Set global variable " + str(match[0]) + "=" + str(match[1])) 2006 self._gvars[match[0]] = match[1] 2007 reply = re.sub(r'<env ' + re.escape(match[0]) + '=' + re.escape(match[1]) + '>', '', reply) 2008 2009 # Global vars 2010 reEnv = re.findall(r'<env (.+?)>', reply) 2011 for match in reEnv: 2012 val = 'undefined' 2013 if match in self._gvars: 2014 val = self._gvars[match] 2015 reply = re.sub(r'<env ' + re.escape(match) + '>', val, reply) 2016 2017 # Streaming code. DEPRECATED! 2018 if '{!' in reply: 2019 self._warn("Use of the {!...} tag is deprecated and not supported here.") 2020 2021 # Set user vars. 2022 reSet = re.findall('<set (.+?)=(.+?)>', reply) 2023 for match in reSet: 2024 self._say("Set uservar " + str(match[0]) + "=" + str(match[1])) 2025 self._users[user][match[0]] = match[1] 2026 reply = re.sub('<set ' + re.escape(match[0]) + '=' + re.escape(match[1]) + '>', '', reply) 2027 2028 # Math tags. 2029 for item in ['add', 'sub', 'mult', 'div']: 2030 matcher = re.findall('<' + item + r' (.+?)=(.+?)>', reply) 2031 for match in matcher: 2032 var = match[0] 2033 value = match[1] 2034 output = '' 2035 2036 # Sanity check the value. 2037 try: 2038 value = int(value) 2039 2040 # So far so good, initialize this one? 2041 if not var in self._users[user]: 2042 self._users[user][var] = 0 2043 except: 2044 output = "[ERR: Math can't '" + item + "' non-numeric value '" + value + "']" 2045 2046 # Attempt the operation. 2047 try: 2048 orig = int(self._users[user][var]) 2049 new = 0 2050 if item == 'add': 2051 new = orig + value 2052 elif item == 'sub': 2053 new = orig - value 2054 elif item == 'mult': 2055 new = orig * value 2056 elif item == 'div': 2057 new = orig / value 2058 self._users[user][var] = new 2059 except: 2060 output = "[ERR: Math couldn't '" + item + "' to value '" + self._users[user][var] + "']" 2061 2062 reply = re.sub('<' + item + ' ' + re.escape(var) + '=' + re.escape(str(value)) + '>', output, reply) 2063 2064 # Get user vars. 2065 reGet = re.findall(r'<get (.+?)>', reply) 2066 for match in reGet: 2067 output = 'undefined' 2068 if match in self._users[user]: 2069 output = self._users[user][match] 2070 reply = re.sub('<get ' + re.escape(match) + '>', str(output), reply) 2071 2072 # Topic setter. 2073 reTopic = re.findall(r'\{topic=(.+?)\}', reply) 2074 for match in reTopic: 2075 self._say("Setting user's topic to " + match) 2076 self._users[user]["topic"] = match 2077 reply = re.sub(r'\{topic=' + re.escape(match) + r'\}', '', reply) 2078 2079 # Inline redirecter. 2080 reRedir = re.findall(r'\{@(.+?)\}', reply) 2081 for match in reRedir: 2082 self._say("Redirect to " + match) 2083 at = match.strip() 2084 subreply = self._getreply(user, at, step=(depth + 1)) 2085 reply = re.sub(r'\{@' + re.escape(match) + r'\}', subreply, reply) 2086 2087 # Object caller. 2088 reCall = re.findall(r'<call>(.+?)</call>', reply) 2089 for match in reCall: 2090 parts = re.split(re_ws, match) 2091 output = '' 2092 obj = parts[0] 2093 args = [] 2094 if len(parts) > 1: 2095 args = parts[1:] 2096 2097 # Do we know this object? 2098 if obj in self._objlangs: 2099 # We do, but do we have a handler for that language? 2100 lang = self._objlangs[obj] 2101 if lang in self._handlers: 2102 # We do. 2103 output = self._handlers[lang].call(self, obj, user, args) 2104 else: 2105 output = '[ERR: No Object Handler]' 2106 else: 2107 output = '[ERR: Object Not Found]' 2108 2109 reply = re.sub('<call>' + re.escape(match) + r'</call>', output, reply) 2110 2111 return reply
2112
2113 - def _string_format(self, msg, method):
2114 """Format a string (upper, lower, formal, sentence).""" 2115 if method == "uppercase": 2116 return msg.upper() 2117 elif method == "lowercase": 2118 return msg.lower() 2119 elif method == "sentence": 2120 return msg.capitalize() 2121 elif method == "formal": 2122 return string.capwords(msg)
2123 2124 ############################################################################ 2125 # Topic Inheritence Utility Methods # 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 # Break if we're in too deep. 2132 if depth > self._depth: 2133 self._warn("Deep recursion while scanning topic inheritence") 2134 2135 # Important info about the depth vs inheritence params to this function: 2136 # depth increments by 1 each time this function recursively calls itself. 2137 # inheritence increments by 1 only when this topic inherits another 2138 # topic. 2139 # 2140 # This way, '> topic alpha includes beta inherits gamma' will have this 2141 # effect: 2142 # alpha and beta's triggers are combined together into one matching 2143 # pool, and then those triggers have higher matching priority than 2144 # gamma's. 2145 # 2146 # The inherited option is True if this is a recursive call, from a topic 2147 # that inherits other topics. This forces the {inherits} tag to be added 2148 # to the triggers. This only applies when the top topic 'includes' 2149 # another topic. 2150 self._say("\tCollecting trigger list for topic " + topic + "(depth=" 2151 + str(depth) + "; inheritence=" + str(inheritence) + "; " 2152 + "inherited=" + str(inherited) + ")") 2153 2154 # topic: the name of the topic 2155 # triglvl: reference to self._topics or self._thats 2156 # depth: starts at 0 and ++'s with each recursion 2157 2158 # Collect an array of triggers to return. 2159 triggers = [] 2160 2161 # Get those that exist in this topic directly. 2162 inThisTopic = [] 2163 if topic in triglvl: 2164 for trigger in triglvl[topic]: 2165 inThisTopic.append(trigger) 2166 2167 # Does this topic include others? 2168 if topic in self._includes: 2169 # Check every included topic. 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 # Does this topic inherit others? 2175 if topic in self._lineage: 2176 # Check every inherited topic. 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 # Collect the triggers for *this* topic. If this topic inherits any 2182 # other topics, it means that this topic's triggers have higher 2183 # priority than those in any inherited topics. Enforce this with an 2184 # {inherits} tag. 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
2194 - def _find_trigger_by_inheritence(self, topic, trig, depth=0):
2195 """Locate the replies for a trigger in an inherited/included topic.""" 2196 2197 # This sub was called because the user matched a trigger from the sorted 2198 # array, but the trigger doesn't belong to their topic, and is instead 2199 # in an inherited or included topic. This is to search for it. 2200 2201 # Prevent recursion. 2202 if depth > self._depth: 2203 self._warn("Deep recursion detected while following an inheritence trail!") 2204 return None 2205 2206 # Inheritence is more important than inclusion: triggers in one topic can 2207 # override those in an inherited topic. 2208 if topic in self._lineage: 2209 for inherits in sorted(self._lineage[topic]): 2210 # See if this inherited topic has our trigger. 2211 if trig in self._topics[inherits]: 2212 # Great! 2213 return self._topics[inherits][trig] 2214 else: 2215 # Check what THAT topic inherits from. 2216 match = self._find_trigger_by_inheritence( 2217 inherits, trig, (depth + 1) 2218 ) 2219 if match: 2220 # Found it! 2221 return match 2222 2223 # See if this topic has an "includes" 2224 if topic in self._includes: 2225 for includes in sorted(self._includes[topic]): 2226 # See if this included topic has our trigger. 2227 if trig in self._topics[includes]: 2228 # Great! 2229 return self._topics[includes][trig] 2230 else: 2231 # Check what THAT topic inherits from. 2232 match = self._find_trigger_by_inheritence( 2233 includes, trig, (depth + 1) 2234 ) 2235 if match: 2236 # Found it! 2237 return match 2238 2239 # Don't know what else to do! 2240 return None
2241
2242 - def _get_topic_tree(self, topic, depth=0):
2243 """Given one topic, get the list of all included/inherited topics.""" 2244 2245 # Break if we're in too deep. 2246 if depth > self._depth: 2247 self._warn("Deep recursion while scanning topic trees!") 2248 return [] 2249 2250 # Collect an array of all topics. 2251 topics = [topic] 2252 2253 # Does this topic include others? 2254 if topic in self._includes: 2255 # Try each of these. 2256 for includes in sorted(self._includes[topic]): 2257 topics.extend(self._get_topic_tree(includes, depth + 1)) 2258 2259 # Does this topic inherit others? 2260 if topic in self._lineage: 2261 # Try each of these. 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 # Miscellaneous Private Methods # 2269 ############################################################################ 2270
2271 - def _is_atomic(self, trigger):
2272 """Determine if a trigger is atomic or not.""" 2273 2274 # Atomic triggers don't contain any wildcards or parenthesis or anything 2275 # of the sort. We don't need to test the full character set, just left 2276 # brackets will do. 2277 special = ['*', '#', '_', '(', '[', '<'] 2278 for char in special: 2279 if char in trigger: 2280 return False 2281 2282 return True
2283
2284 - def _word_count(self, trigger, all=False):
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 # Word count 2293 for word in words: 2294 if len(word) > 0: 2295 wc += 1 2296 2297 return wc
2298
2299 - def _strip_nasties(self, s):
2300 """Formats a string for ASCII regex matching.""" 2301 s = re.sub(re_nasties, '', s) 2302 return s
2303
2304 - def _dump(self):
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 # Interactive Mode # 2339 ################################################################################ 2340 2341 if __name__ == "__main__": 2342 from interactive import interactive_mode 2343 interactive_mode() 2344 2345 # vim:expandtab 2346