├── .gitignore ├── LICENSE ├── README.md ├── ai.py ├── aiml ├── AimlParser.py ├── DefaultSubs.py ├── Kernel.py ├── LangSupport.py ├── PatternMgr.py ├── Utils.py ├── WordSub.py └── __init__.py ├── aiml_set ├── 20q.aiml ├── Computers.aiml ├── Date.aiml ├── Geography.aiml ├── Happy.aiml ├── Knowledge.aiml ├── Science.aiml ├── binary.aiml ├── bornin.aiml ├── calendar.aiml ├── general.aiml ├── jokes.aiml ├── learn.aiml ├── maimeng.aiml ├── math.aiml ├── rude.aiml └── sex.aiml ├── app.py ├── config.py ├── handlers.py ├── plugins ├── __init__.py ├── feed.py ├── ip.py ├── oschina.py ├── simsimi.py ├── talkbot.py └── v2ex.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | wechat.conf 2 | talkbot.brn 3 | .DS_Store 4 | 5 | #PyCharm 6 | .idea 7 | .ropeproject 8 | 9 | *~ 10 | *.py[cod] 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Packages 16 | *.egg 17 | *.egg-info 18 | dist 19 | build 20 | eggs 21 | parts 22 | bin 23 | var 24 | sdist 25 | develop-eggs 26 | .installed.cfg 27 | lib 28 | lib64 29 | include 30 | .Python 31 | 32 | # Installer logs 33 | pip-log.txt 34 | 35 | # Unit test / coverage reports 36 | .coverage 37 | .tox 38 | nosetests.xml 39 | 40 | # Translations 41 | *.mo 42 | 43 | # Mr Developer 44 | .mr.developer.cfg 45 | .project 46 | .pydevproject 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 messense 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wechat-bot 2 | 3 | A robot of wechat based on python 4 | 5 | ## How to run 6 | 7 | First, clone the latest source from github: 8 | 9 | ```bash 10 | $ git clone https://github.com/messense/wechat-bot.git 11 | ``` 12 | 13 | Then, install the required packages using pip: 14 | 15 | ```bash 16 | $ [sudo] pip install -r requirements.txt 17 | ``` 18 | 19 | Create a text file named wechat.conf in the source code directory, write the configuration and save. For example: 20 | 21 | ```python 22 | #coding=utf-8 23 | debug = False 24 | port = 8888 25 | token = 'your token' 26 | username = 'your username' 27 | simsimi_key = '' 28 | talkbot_brain_path = 'talkbot.brn' 29 | ``` 30 | 31 | Now, let's start the server: 32 | 33 | ```bash 34 | $ python app.py 35 | ``` 36 | 37 | ## License 38 | 39 | wechat-bot published under the [MIT](http://opensource.org/licenses/MIT) license. 40 | 41 | Copyright (c) 2013 messense 42 | 43 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 44 | 45 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 46 | 47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /ai.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import logging 4 | import plugins 5 | 6 | 7 | class AI(object): 8 | 9 | _plugin_modules = [] 10 | _plugin_loaded = False 11 | 12 | def __init__(self, msg=None): 13 | if msg: 14 | self.id = msg.source 15 | 16 | @classmethod 17 | def load_plugins(cls): 18 | if cls._plugin_loaded: 19 | return 20 | for name in plugins.__all__: 21 | try: 22 | __import__('plugins.%s' % name) 23 | cls.add_plugin(getattr(plugins, name)) 24 | logging.info('Plugin %s loaded success.' % name) 25 | except: 26 | logging.warning('Fail to load plugin %s' % name) 27 | cls._plugin_loaded = True 28 | 29 | @classmethod 30 | def add_plugin(cls, plugin): 31 | if not hasattr(plugin, 'test'): 32 | logging.error('Plugin %s has no method named test, ignore it') 33 | return False 34 | if not hasattr(plugin, 'respond'): 35 | logging.error('Plugin %s has no method named respond, ignore it') 36 | return False 37 | cls._plugin_modules.append(plugin) 38 | return True 39 | 40 | def respond(self, data, msg=None): 41 | response = None 42 | for plugin in self._plugin_modules: 43 | try: 44 | if plugin.test(data, msg, self): 45 | response = plugin.respond(data, msg, self) 46 | except: 47 | logging.warning('Plugin %s failed to respond', plugin.__name__) 48 | continue 49 | if response: 50 | logging.info('Plugin %s respond successfully', plugin.__name__) 51 | return response 52 | 53 | return response or u'呵呵' 54 | 55 | 56 | AI.load_plugins() 57 | 58 | if __name__ == '__main__': 59 | bot = AI() 60 | print(bot.respond('hello')) 61 | -------------------------------------------------------------------------------- /aiml/AimlParser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from xml.sax.handler import ContentHandler 3 | from xml.sax.xmlreader import Locator 4 | import sys 5 | import xml.sax 6 | import xml.sax.handler 7 | 8 | class AimlParserError(Exception): pass 9 | 10 | class AimlHandler(ContentHandler): 11 | # The legal states of the AIML parser 12 | _STATE_OutsideAiml = 0 13 | _STATE_InsideAiml = 1 14 | _STATE_InsideCategory = 2 15 | _STATE_InsidePattern = 3 16 | _STATE_AfterPattern = 4 17 | _STATE_InsideThat = 5 18 | _STATE_AfterThat = 6 19 | _STATE_InsideTemplate = 7 20 | _STATE_AfterTemplate = 8 21 | 22 | def __init__(self, encoding = "UTF-8"): 23 | self.categories = {} 24 | self._encoding = encoding 25 | self._state = self._STATE_OutsideAiml 26 | self._version = "" 27 | self._namespace = "" 28 | self._forwardCompatibleMode = False 29 | self._currentPattern = "" 30 | self._currentThat = "" 31 | self._currentTopic = "" 32 | self._insideTopic = False 33 | self._currentUnknown = "" # the name of the current unknown element 34 | 35 | # This is set to true when a parse error occurs in a category. 36 | self._skipCurrentCategory = False 37 | 38 | # Counts the number of parse errors in a particular AIML document. 39 | # query with getNumErrors(). If 0, the document is AIML-compliant. 40 | self._numParseErrors = 0 41 | 42 | # TODO: select the proper validInfo table based on the version number. 43 | self._validInfo = self._validationInfo101 44 | 45 | # This stack of bools is used when parsing
  • elements inside 46 | # elements, to keep track of whether or not an 47 | # attribute-less "default"
  • element has been found yet. Only 48 | # one default
  • is allowed in each element. We need 49 | # a stack in order to correctly handle nested tags. 50 | self._foundDefaultLiStack = [] 51 | 52 | # This stack of strings indicates what the current whitespace-handling 53 | # behavior should be. Each string in the stack is either "default" or 54 | # "preserve". When a new AIML element is encountered, a new string is 55 | # pushed onto the stack, based on the value of the element's "xml:space" 56 | # attribute (if absent, the top of the stack is pushed again). When 57 | # ending an element, pop an object off the stack. 58 | self._whitespaceBehaviorStack = ["default"] 59 | 60 | self._elemStack = [] 61 | self._locator = Locator() 62 | self.setDocumentLocator(self._locator) 63 | 64 | def getNumErrors(self): 65 | "Return the number of errors found while parsing the current document." 66 | return self._numParseErrors 67 | 68 | def setEncoding(self, encoding): 69 | """Set the text encoding to use when encoding strings read from XML. 70 | 71 | Defaults to 'UTF-8'. 72 | 73 | """ 74 | self._encoding = encoding 75 | 76 | def _location(self): 77 | "Return a string describing the current location in the source file." 78 | line = self._locator.getLineNumber() 79 | column = self._locator.getColumnNumber() 80 | return "(line %d, column %d)" % (line, column) 81 | 82 | def _pushWhitespaceBehavior(self, attr): 83 | """Push a new string onto the whitespaceBehaviorStack. 84 | 85 | The string's value is taken from the "xml:space" attribute, if it exists 86 | and has a legal value ("default" or "preserve"). Otherwise, the previous 87 | stack element is duplicated. 88 | 89 | """ 90 | assert len(self._whitespaceBehaviorStack) > 0, "Whitespace behavior stack should never be empty!" 91 | try: 92 | if attr["xml:space"] == "default" or attr["xml:space"] == "preserve": 93 | self._whitespaceBehaviorStack.append(attr["xml:space"]) 94 | else: 95 | raise AimlParserError, "Invalid value for xml:space attribute "+self._location() 96 | except KeyError: 97 | self._whitespaceBehaviorStack.append(self._whitespaceBehaviorStack[-1]) 98 | 99 | def startElementNS(self, name, qname, attr): 100 | print "QNAME:", qname 101 | print "NAME:", name 102 | uri,elem = name 103 | if (elem == "bot"): print "name:", attr.getValueByQName("name"), "a'ite?" 104 | self.startElement(elem, attr) 105 | pass 106 | 107 | def startElement(self, name, attr): 108 | # Wrapper around _startElement, which catches errors in _startElement() 109 | # and keeps going. 110 | 111 | # If we're inside an unknown element, ignore everything until we're 112 | # out again. 113 | if self._currentUnknown != "": 114 | return 115 | # If we're skipping the current category, ignore everything until 116 | # it's finished. 117 | if self._skipCurrentCategory: 118 | return 119 | 120 | # process this start-element. 121 | try: self._startElement(name, attr) 122 | except AimlParserError, msg: 123 | # Print the error message 124 | sys.stderr.write("PARSE ERROR: %s\n" % msg) 125 | 126 | self._numParseErrors += 1 # increment error count 127 | # In case of a parse error, if we're inside a category, skip it. 128 | if self._state >= self._STATE_InsideCategory: 129 | self._skipCurrentCategory = True 130 | 131 | def _startElement(self, name, attr): 132 | if name == "aiml": 133 | # tags are only legal in the OutsideAiml state 134 | if self._state != self._STATE_OutsideAiml: 135 | raise AimlParserError, "Unexpected tag "+self._location() 136 | self._state = self._STATE_InsideAiml 137 | self._insideTopic = False 138 | self._currentTopic = u"" 139 | try: self._version = attr["version"] 140 | except KeyError: 141 | # This SHOULD be a syntax error, but so many AIML sets out there are missing 142 | # "version" attributes that it just seems nicer to let it slide. 143 | #raise AimlParserError, "Missing 'version' attribute in tag "+self._location() 144 | #print "WARNING: Missing 'version' attribute in tag "+self._location() 145 | #print " Defaulting to version 1.0" 146 | self._version = "1.0" 147 | self._forwardCompatibleMode = (self._version != "1.0.1") 148 | self._pushWhitespaceBehavior(attr) 149 | # Not sure about this namespace business yet... 150 | #try: 151 | # self._namespace = attr["xmlns"] 152 | # if self._version == "1.0.1" and self._namespace != "http://alicebot.org/2001/AIML-1.0.1": 153 | # raise AimlParserError, "Incorrect namespace for AIML v1.0.1 "+self._location() 154 | #except KeyError: 155 | # if self._version != "1.0": 156 | # raise AimlParserError, "Missing 'version' attribute(s) in tag "+self._location() 157 | elif self._state == self._STATE_OutsideAiml: 158 | # If we're outside of an AIML element, we ignore all tags. 159 | return 160 | elif name == "topic": 161 | # tags are only legal in the InsideAiml state, and only 162 | # if we're not already inside a topic. 163 | if (self._state != self._STATE_InsideAiml) or self._insideTopic: 164 | raise AimlParserError, "Unexpected tag", self._location() 165 | try: self._currentTopic = unicode(attr['name']) 166 | except KeyError: 167 | raise AimlParserError, "Required \"name\" attribute missing in element "+self._location() 168 | self._insideTopic = True 169 | elif name == "category": 170 | # tags are only legal in the InsideAiml state 171 | if self._state != self._STATE_InsideAiml: 172 | raise AimlParserError, "Unexpected tag "+self._location() 173 | self._state = self._STATE_InsideCategory 174 | self._currentPattern = u"" 175 | self._currentThat = u"" 176 | # If we're not inside a topic, the topic is implicitly set to * 177 | if not self._insideTopic: self._currentTopic = u"*" 178 | self._elemStack = [] 179 | self._pushWhitespaceBehavior(attr) 180 | elif name == "pattern": 181 | # tags are only legal in the InsideCategory state 182 | if self._state != self._STATE_InsideCategory: 183 | raise AimlParserError, "Unexpected tag "+self._location() 184 | self._state = self._STATE_InsidePattern 185 | elif name == "that" and self._state == self._STATE_AfterPattern: 186 | # are legal either inside a