├── README.md ├── setup.py └── vlang ├── LICENSE ├── LICENSE~ ├── README.md ├── __init__.py ├── menu.py ├── message.py ├── parser.py ├── server.py ├── setup.py~ ├── user.py └── view.py /README.md: -------------------------------------------------------------------------------- 1 | # vLang 使用说明 # 2 | 3 | vLang是一个基于模型的微信开发框架,遵循MIT协议发布。 4 | 5 | # 安装 6 | 7 | 开发与测试环境: 8 | Ubuntu Kylin 15.04 64bit 9 | Python 3.4 10 | Tornado 4.1 11 | > python3 setup.py install 12 | 13 | # 入门 # 14 | 15 | ### 1. Hello World! ### 16 | 17 | 最简单的例程,对用户发送的所有消息,回复“Hello World!” 18 | 19 | ```python 20 | from vlang.menu import Menu 21 | from vlang.server import start 22 | 23 | class MainMenu(Menu): 24 | def action(self): 25 | yield "Hello World" 26 | 27 | # token需和微信后台填写一致 28 | settings = {"url" :"/weixin", 29 | "port" : 8080, 30 | "token" : "WQif56gU" } 31 | start(MainMenu(), settings) 32 | ``` 33 | 34 | ### 2. 微信计算器 ### 35 | 36 | + 简易乘法计算器 37 | 38 | ```python 39 | from vlang.menu import Menu 40 | from vlang.server import start 41 | 42 | class MainMenu(Menu): 43 | def action(self): 44 | num0 = yield "请输入因数1:" #用户输入将保存在num0里. 45 | num1 = yield "请输入因数2:" 46 | num2 = float(num0) * float(num1) 47 | yield "计算结果:\n {0} x {1} = {2}".format(num0,num1,num2) 48 | 49 | settings = {"url" :"/weixin", 50 | "port" : 8080, 51 | "token" : "WQif56gU" } 52 | start(MainMenu(), settings) 53 | ``` 54 | 55 | + 带有二级级菜单的四则运算器,能计算加减法,乘法。 56 | 57 | ```python 58 | from vlang.menu import Menu 59 | from vlang.server import start 60 | 61 | class MainMenu(Menu): 62 | def tag(self): 63 | self.name = "计算器" 64 | def action(self): 65 | reply = yield self.makeMenu() # 根据子菜单的名字,生成菜单列表 66 | yield self.autoJump(reply) # 根据回复,跳转到对应菜单 67 | 68 | class Menu_1(Menu): 69 | def tag(self): 70 | self.name = "加减法" 71 | def action(self): 72 | reply = yield self.makeMenu() 73 | yield self.autoJump(reply) 74 | 75 | class Menu_1_1(Menu): 76 | def tag(self): 77 | self.name = "加法" 78 | def action(self): 79 | num0 = yield "请输入加数1:" 80 | num1 = yield "请输入加数2:" 81 | num2 = float(num0) + float(num1) 82 | yield "计算结果:\n {0} + {1} = {2}".format(num0, num1, num2) 83 | 84 | class Menu_1_2(Menu): 85 | def tag(self): 86 | self.name = "减法" 87 | def action(self): 88 | num0 = yield "请输入被减数:" 89 | num1 = yield "请输入减数:" 90 | num2 = float(num0) - float(num1) 91 | reply = yield '''计算结果:\n {0} - {1} = {2} 92 | 回复“ 0 ”可回到主菜单'''.format(num0, num1, num2) 93 | if reply == "0" : yield MainMenu # 跳转到指定菜单 94 | 95 | class Menu_2(Menu): 96 | def tag(self): 97 | self.name = "乘法" 98 | def action(self): 99 | num0 = yield "请输入因数1:" 100 | num1 = yield "请输入因数2:" 101 | num2 = float(num0) * float(num1) 102 | yield "计算结果:\n {0} x {1} = {2}".format(num0, num1, num2) 103 | 104 | # 链接菜单 105 | menuTree = MainMenu() # 新建一个菜单树 106 | menuTree.add(Menu_1) # 链接一级菜单 107 | menuTree.add(Menu_2) 108 | menuTree.Menu_1.add(Menu_1_1) # 链接二级菜单 109 | menuTree.Menu_1.add(Menu_1_2) 110 | 111 | # 设置并启动服务器 112 | settings = {"url" :"/weixin", 113 | "port" : 8080, 114 | "token" : "WQif56gU" } 115 | start(menuTree, settings) 116 | ``` 117 | 118 | # Menu # 119 | 120 | 在vLang中,微信项目是由一个个菜单链接而构成的, 本节将详细介绍vLang中菜单的结构,链接,启动。 121 | 122 | ### 结构 ### 123 | 124 | 菜单是继承自vlang.menu.Menu的类,并需重载其action()方法,用来接收,处理,回复微信消息。 125 | 在多级菜单中,还需重载tag()方法,用以保存一些设置,目前仅有self.name这一项,用以指定菜单的名字。 126 | 127 | ```python 128 | from vlang.menu import Menu 129 | 130 | class MainMenu(Menu): 131 | def tag(self): 132 | #用以保存本菜单的设置。 133 | self.name = "主菜单" 134 | 135 | def action(self): 136 | #当收到用户消息时,vLang会调用对应菜单的action(),并传入用户消息。 137 | pass 138 | 139 | ``` 140 | 141 | ### 链接 ### 142 | 143 | 为了简化开发,vLang支持多级菜单。菜单与其子菜单的链接使用add(Menu)。 144 | 在上文 *四则运算器* 一例中, 145 | 146 | ```python 147 | menuTree = MainMenu() #新建一个菜单树,必须为Menu类的实例。 148 | menuTree.add(Menu_1) #链接一级菜单 149 | menuTree.add(Menu_2) 150 | menuTree.Menu_1.add(Menu_1_1) #链接二级菜单 151 | menuTree.Menu_1.add(Menu_1_2) 152 | ``` 153 | 154 | ### 启动 ### 155 | 156 | ```python 157 | vlang.server.start(menuTree, settings) 158 | ``` 159 | 其中, 160 | menuTree是Menu类的实例,如果有多级菜单,启动前需先链接菜单。 161 | settings是一个包含许多设置的字典 162 | ```python 163 | settings = {"url" :"/weixin", 164 | "port" : 8080, #默认为 80 端口 165 | "ip":"127.0.0.1", #默认为"" , 绑定所有地址, 166 | "token" : "WQif56gU",#在微信公众号官网设置的token 167 | "work" : "WQif56gU"} #可同时服务的用户数量,也是线程数量,默认为10 168 | 169 | ``` 170 | 171 | # yield! # 172 | Python中 *yield* 关键字具有神奇的魔力。 173 | vLang中,yield用于: *回复/接收消息*,*跳转菜单* 174 | 175 | ### 回复 ### 176 | 177 | + **给用户发送消息:** 178 | 179 | ```python 180 | yield "您好,欢迎光临!" 181 | ``` 182 | 183 | + **给用户发送消息,并得到回复:** 184 | 185 | ```python 186 | reply = yield "请输入用户名:" 187 | ``` 188 | 189 | + **缓存一段话,暂不发给用户,程序将继续执行至下一个yield语句处。一般用于缓存一段错误信息:** 190 | 191 | ```python 192 | def action(self): 193 | while(True): 194 | try: 195 | reply = yield "请输入整数:" 196 | num = int(reply) 197 | break 198 | except ValueError: 199 | yield "您输入的不是整数,请重试\n", self.BUFFER # 缓存一段话,后面添加个self.buffer即可。 200 | ``` 201 | 202 | 如果用户输入了"haha",int(reply)将抛出ValueError异常 203 | 程序实际回复: 204 | >您输入的不是整数,请重试 205 | >请输入整数: 206 | 207 | 可缓存多段文本,当程序继续执行到没有self.buffer的yield语句时,vLang才会将缓存区文本发送给用户。 208 | 209 | ### 跳转 ### 210 | 以 *入门* 小节中,*四则运算器* 为例。 211 | 212 | + **跳转到另一个菜单:** 213 | 214 | ```python 215 | yield Menu_1_2 216 | ``` 217 | 218 | 能够传入Menu,或者Menu的实例。 219 | 例如,跳转到主菜单可以这样写: 220 | 221 | ```python 222 | yield MainMenu 223 | ``` 224 | 225 | 也可以这样写: 226 | 227 | ```python 228 | yield menuTree 229 | ``` 230 | 231 | + **跳转到现行菜单的开始点:** 232 | 233 | ```python 234 | yield self 235 | ``` 236 | 237 | 特别的,如果没有显式指定下一个菜单,当action() 执行完后,默认跳转到现行菜单的开始点。 238 | 239 | + **跳转到父菜单:** 240 | ```python 241 | yield self.baseMenu 242 | ``` 243 | 244 | 245 | + **当然,跳转菜单的同时也能给用户回复消息,也缓存一段信息:** 246 | 247 | ```python 248 | yield "充值失败!", MainMenu , self.buffer 249 | ``` 250 | 251 | 不过,上面的三个参数得 **按顺序** 写。其原型是:(String , Menu , self.buffer) 252 | 253 | # 版本 # 254 | 目前版本为 0.2.1 ,是开源的第一个版本。 255 | 256 | * 下个版本中,将会有如下**改进**: 257 | 258 | * autoJump() 支持模糊匹配 259 | * 加入日志模块 260 | * 加入消息加解密功能 261 | * 支持多媒体消息,如语音,图片,图文,地理位置等 262 | 263 | * 未来版本中,可能有如下**改进**: 264 | 265 | * 在80端口处提供一个管理员网页,方便设置,管理 266 | * 提供幸运大转盘,刮刮卡等等常见活动 267 | * 接入 其他开源项目,以支持中文分词和情感分析 268 | * …… 269 | * 非常期待您的建议! 270 | 271 | # 关于 # 272 | **谢谢您的耐心阅读!** 273 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | setup( 3 | name = "vlang", 4 | version = "0.2.1", 5 | keywords = "wechat weixin werobot", 6 | packages = find_packages(), 7 | author = "sPeng", 8 | author_email = "ss@uutoto.com", 9 | description = "基于模型的微信开发框架,vlang" 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /vlang/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 uutoto 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. 8 | -------------------------------------------------------------------------------- /vlang/LICENSE~: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 uutoto 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. 8 | -------------------------------------------------------------------------------- /vlang/README.md: -------------------------------------------------------------------------------- 1 | # vLang 使用说明 # 2 | 3 | vLang是一个基于模型的微信开发框架,遵循MIT协议发布。 4 | 5 | # 安装 6 | 7 | 开发与测试环境: 8 | Ubuntu Kylin 15.04 64bit 9 | Python 3.4 10 | Tornado 4.1 11 | > python3 setup.py install 12 | 13 | # 入门 # 14 | 15 | ### 1. Hello World! ### 16 | 17 | 最简单的例程,对用户发送的所有消息,回复“Hello World!” 18 | 19 | ```python 20 | from vlang.menu import Menu 21 | from vlang.server import start 22 | 23 | class MainMenu(Menu): 24 | def action(self): 25 | yield "Hello World" 26 | 27 | # token需和微信后台填写一致 28 | settings = {"url" :"/weixin", 29 | "port" : 8080, 30 | "token" : "WQif56gU" } 31 | start(MainMenu(), settings) 32 | ``` 33 | 34 | ### 2. 微信计算器 ### 35 | 36 | + 简易乘法计算器 37 | 38 | ```python 39 | from vlang.menu import Menu 40 | from vlang.server import start 41 | 42 | class MainMenu(Menu): 43 | def action(self): 44 | num0 = yield "请输入因数1:" #用户输入将保存在num0里. 45 | num1 = yield "请输入因数2:" 46 | num2 = float(num0) * float(num1) 47 | yield "计算结果:\n {0} x {1} = {2}".format(num0,num1,num2) 48 | 49 | settings = {"url" :"/weixin", 50 | "port" : 8080, 51 | "token" : "WQif56gU" } 52 | start(MainMenu(), settings) 53 | ``` 54 | 55 | + 带有二级级菜单的四则运算器,能计算加减法,乘法。 56 | 57 | ```python 58 | from vlang.menu import Menu 59 | from vlang.server import start 60 | 61 | class MainMenu(Menu): 62 | def tag(self): 63 | self.name = "计算器" 64 | def action(self): 65 | reply = yield self.makeMenu() # 根据子菜单的名字,生成菜单列表 66 | yield self.autoJump(reply) # 根据回复,跳转到对应菜单 67 | 68 | class Menu_1(Menu): 69 | def tag(self): 70 | self.name = "加减法" 71 | def action(self): 72 | reply = yield self.makeMenu() 73 | yield self.autoJump(reply) 74 | 75 | class Menu_1_1(Menu): 76 | def tag(self): 77 | self.name = "加法" 78 | def action(self): 79 | num0 = yield "请输入加数1:" 80 | num1 = yield "请输入加数2:" 81 | num2 = float(num0) + float(num1) 82 | yield "计算结果:\n {0} + {1} = {2}".format(num0, num1, num2) 83 | 84 | class Menu_1_2(Menu): 85 | def tag(self): 86 | self.name = "减法" 87 | def action(self): 88 | num0 = yield "请输入被减数:" 89 | num1 = yield "请输入减数:" 90 | num2 = float(num0) - float(num1) 91 | reply = yield '''计算结果:\n {0} - {1} = {2} 92 | 回复“ 0 ”可回到主菜单'''.format(num0, num1, num2) 93 | if reply == "0" : yield MainMenu # 跳转到指定菜单 94 | 95 | class Menu_2(Menu): 96 | def tag(self): 97 | self.name = "乘法" 98 | def action(self): 99 | num0 = yield "请输入因数1:" 100 | num1 = yield "请输入因数2:" 101 | num2 = float(num0) * float(num1) 102 | yield "计算结果:\n {0} x {1} = {2}".format(num0, num1, num2) 103 | 104 | # 链接菜单 105 | menuTree = MainMenu() # 新建一个菜单树 106 | menuTree.add(Menu_1) # 链接一级菜单 107 | menuTree.add(Menu_2) 108 | menuTree.Menu_1.add(Menu_1_1) # 链接二级菜单 109 | menuTree.Menu_1.add(Menu_1_2) 110 | 111 | # 设置并启动服务器 112 | settings = {"url" :"/weixin", 113 | "port" : 8080, 114 | "token" : "WQif56gU" } 115 | start(menuTree, settings) 116 | ``` 117 | 118 | # Menu # 119 | 120 | 在vLang中,微信项目是由一个个菜单链接而构成的, 本节将详细介绍vLang中菜单的结构,链接,启动。 121 | 122 | ### 结构 ### 123 | 124 | 菜单是继承自vlang.menu.Menu的类,并需重载其action()方法,用来接收,处理,回复微信消息。 125 | 在多级菜单中,还需重载tag()方法,用以保存一些设置,目前仅有self.name这一项,用以指定菜单的名字。 126 | 127 | ```python 128 | from vlang.menu import Menu 129 | 130 | class MainMenu(Menu): 131 | def tag(self): 132 | #用以保存本菜单的设置。 133 | self.name = "主菜单" 134 | 135 | def action(self): 136 | #当收到用户消息时,vLang会调用对应菜单的action(),并传入用户消息。 137 | pass 138 | 139 | ``` 140 | 141 | ### 链接 ### 142 | 143 | 为了简化开发,vLang支持多级菜单。菜单与其子菜单的链接使用add(Menu)。 144 | 在上文 *四则运算器* 一例中, 145 | 146 | ```python 147 | menuTree = MainMenu() #新建一个菜单树,必须为Menu类的实例。 148 | menuTree.add(Menu_1) #链接一级菜单 149 | menuTree.add(Menu_2) 150 | menuTree.Menu_1.add(Menu_1_1) #链接二级菜单 151 | menuTree.Menu_1.add(Menu_1_2) 152 | ``` 153 | 154 | ### 启动 ### 155 | 156 | ```python 157 | vlang.server.start(menuTree, settings) 158 | ``` 159 | 其中, 160 | menuTree是Menu类的实例,如果有多级菜单,启动前需先链接菜单。 161 | settings是一个包含许多设置的字典 162 | ```python 163 | settings = {"url" :"/weixin", 164 | "port" : 8080, #默认为 80 端口 165 | "ip":"127.0.0.1", #默认为"" , 绑定所有地址, 166 | "token" : "WQif56gU",#在微信公众号官网设置的token 167 | "work" : "WQif56gU"} #可同时服务的用户数量,也是线程数量,默认为10 168 | 169 | ``` 170 | 171 | # yield! # 172 | Python中 *yield* 关键字具有神奇的魔力。 173 | vLang中,yield用于: *回复/接收消息*,*跳转菜单* 174 | 175 | ### 回复 ### 176 | 177 | + **给用户发送消息:** 178 | 179 | ```python 180 | yield "您好,欢迎光临!" 181 | ``` 182 | 183 | + **给用户发送消息,并得到回复:** 184 | 185 | ```python 186 | reply = yield "请输入用户名:" 187 | ``` 188 | 189 | + **缓存一段话,暂不发给用户,程序将继续执行至下一个yield语句处。一般用于缓存一段错误信息:** 190 | 191 | ```python 192 | def action(self): 193 | while(True): 194 | try: 195 | reply = yield "请输入整数:" 196 | num = int(reply) 197 | break 198 | except ValueError: 199 | yield "您输入的不是整数,请重试\n", self.BUFFER # 缓存一段话,后面添加个self.buffer即可。 200 | ``` 201 | 202 | 如果用户输入了"haha",int(reply)将抛出ValueError异常 203 | 程序实际回复: 204 | >您输入的不是整数,请重试 205 | >请输入整数: 206 | 207 | 可缓存多段文本,当程序继续执行到没有self.buffer的yield语句时,vLang才会将缓存区文本发送给用户。 208 | 209 | ### 跳转 ### 210 | 以 *入门* 小节中,*四则运算器* 为例。 211 | 212 | + **跳转到另一个菜单:** 213 | 214 | ```python 215 | yield Menu_1_2 216 | ``` 217 | 218 | 能够传入Menu,或者Menu的实例。 219 | 例如,跳转到主菜单可以这样写: 220 | 221 | ```python 222 | yield MainMenu 223 | ``` 224 | 225 | 也可以这样写: 226 | 227 | ```python 228 | yield menuTree 229 | ``` 230 | 231 | + **跳转到现行菜单的开始点:** 232 | 233 | ```python 234 | yield self 235 | ``` 236 | 237 | 特别的,如果没有显式指定下一个菜单,当action() 执行完后,默认跳转到现行菜单的开始点。 238 | 239 | + **跳转到父菜单:** 240 | ```python 241 | yield self.baseMenu 242 | ``` 243 | 244 | 245 | + **当然,跳转菜单的同时也能给用户回复消息,也缓存一段信息:** 246 | 247 | ```python 248 | yield "充值失败!", MainMenu , self.buffer 249 | ``` 250 | 251 | 不过,上面的三个参数得 **按顺序** 写。其原型是:(String , Menu , self.buffer) 252 | 253 | # 版本 # 254 | 目前版本为 0.2.1 ,是开源的第一个版本。 255 | 256 | * 下个版本中,将会有如下**改进**: 257 | 258 | * autoJump() 支持模糊匹配 259 | * 加入日志模块 260 | * 加入消息加解密功能 261 | * 支持多媒体消息,如语音,图片,图文,地理位置等 262 | 263 | * 未来版本中,可能有如下**改进**: 264 | 265 | * 在80端口处提供一个管理员网页,方便设置,管理 266 | * 提供幸运大转盘,刮刮卡等等常见活动 267 | * 接入 其他开源项目,以支持中文分词和情感分析 268 | * …… 269 | * 非常期待您的建议! 270 | 271 | # 关于 # 272 | **谢谢您的耐心阅读!** -------------------------------------------------------------------------------- /vlang/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/speng4096/vlang/237a608d2fb42112b2bae16b37eff4c68878d321/vlang/__init__.py -------------------------------------------------------------------------------- /vlang/menu.py: -------------------------------------------------------------------------------- 1 | from vlang.message import Send 2 | 3 | # 为了方面扩展,独立出Module模块,后面加入功能 4 | class Module(Send): 5 | class BUFFER(): 6 | pass 7 | 8 | class Menu(Module): 9 | def __init__(self): 10 | self.name = "" 11 | self.subMenus = [] 12 | self.nextMenu = None 13 | self.tag() 14 | self.baseMenu = self # 默认父菜单是自己,这会在链接菜单时被修改 15 | 16 | def add(self, subMenu): 17 | new = subMenu() 18 | self.subMenus.append(new) 19 | new.baseMenu = self 20 | setattr(self, subMenu.__name__, new) 21 | 22 | def action(self): 23 | '''用户进入菜单后的动作''' 24 | pass 25 | 26 | def tag(self): 27 | '''对菜单的设置,包括name等等''' 28 | pass 29 | 30 | def makeMenu(self): 31 | '''返回已格式化的菜单''' 32 | menuList = self.getSubMenusName() 33 | menuStr = self.name + "\n" 34 | f = "{0}:{1};\n" 35 | for index, name in enumerate(menuList): 36 | menuStr += f.format(index + 1, name) 37 | menuStr += "请回复数字:\n" 38 | return menuStr 39 | 40 | def autoJump(self, reply): 41 | '''根据用户回复,跳转菜单,暂是只支持数字,后面可加入模糊匹配 42 | :失败重新打印菜单''' 43 | try: 44 | index = int(reply) - 1 # 菜单从1开始,列表从0开始 45 | return self.getSubMenus()[index] 46 | except (IndexError, ValueError): 47 | return "没有这个菜单\n\n", self, self.BUFFER 48 | 49 | def getSubMenus(self): 50 | '''返回list,包含本菜单下的的直系子菜单的引用''' 51 | return self.subMenus 52 | 53 | def getSubMenusName(self): 54 | '''返回list,包含本菜单下的的直系子菜单的名称''' 55 | return [m.name for m in self.subMenus] 56 | 57 | 58 | def getSubMenusRec(self): 59 | '''递归寻找子菜单,返回list,包含所有子菜单的引用''' 60 | return self._getSubMenusRec(self.subMenus) 61 | 62 | def getSubMenusNameRec(self): 63 | '''递归寻找子菜单,返回list,包含所有子菜单的名称''' 64 | return self._getSubMenusNameRec(self.subMenus) 65 | 66 | def _getSubMenusNameRec(self, subMenus): 67 | '''递归寻找子菜单''' 68 | tmp = [] 69 | for m in subMenus: 70 | if m.subMenus: 71 | tmp.append(self._getSubMenusNameRec(m.subMenus)) 72 | else: 73 | tmp.append(m.name) 74 | return tmp 75 | 76 | def _getSubMenusRec(self, subMenus): 77 | '''递归寻找子菜单''' 78 | tmp = [] 79 | for m in subMenus: 80 | if m.subMenus: 81 | tmp.append(self._getSubMenusRec(m.subMenus)) 82 | else: 83 | tmp.append(m) 84 | return tmp 85 | 86 | def _getListRec(self, arr): 87 | lit = [] 88 | for i in arr: 89 | if isinstance(i, list): 90 | lit += self._getListRec(i) 91 | else: 92 | lit += [i] 93 | return lit 94 | 95 | def getMenuTable(self): 96 | tmp = self._getListRec(self.getSubMenusRec()) 97 | tmp += [self] 98 | tmp = {i.__class__.__name__:i for i in tmp} 99 | return tmp 100 | -------------------------------------------------------------------------------- /vlang/message.py: -------------------------------------------------------------------------------- 1 | import time 2 | class Send(): 3 | def textMsg(self, content): 4 | template = """ 5 | 6 | 7 | 8 | {CreateTime} 9 | 10 | 11 | 12 | """ 13 | args = {} 14 | args["CreateTime"] = int(time.time()) 15 | args["Content"] = content 16 | return template.format(**args) -------------------------------------------------------------------------------- /vlang/parser.py: -------------------------------------------------------------------------------- 1 | from xml.etree import ElementTree as ET 2 | from vlang.user import User 3 | 4 | userTable = {} 5 | 6 | def parserRaw(rawXML, mainMenu): 7 | '''把微信发来的xml格式的消息,解析成dict格式,方便在python中操作''' 8 | '''将消息传递给对应的User实例处理''' 9 | if not rawXML: 10 | return 11 | message = dict((child.tag, child.text) for child in ET.fromstring(rawXML)) 12 | 13 | # 根据微信ID寻找user实例,如果没有实例就new一个 14 | openID = message['FromUserName'] 15 | if openID not in userTable: 16 | userTable[openID] = User(mainMenu) 17 | 18 | userTable[openID].buffer = "" 19 | return userTable[openID].handle(message) 20 | -------------------------------------------------------------------------------- /vlang/server.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | import tornado.ioloop 3 | import hashlib 4 | from vlang.parser import parserRaw 5 | from tornado.concurrent import run_on_executor 6 | from concurrent.futures import ThreadPoolExecutor 7 | 8 | maxWorks = 10 9 | # 启动服务器 10 | def start(mainMenu, settings): 11 | global maxWorks 12 | maxWorks = settings.get('maxWorks', 10) 13 | application = tornado.web.Application( 14 | handlers=[(settings['url'], Server, {"mainMenu":mainMenu, "token":settings['token']})]) 15 | application.listen(settings.get('port', 80), settings.get('ip', '')) 16 | tornado.ioloop.IOLoop.instance().start() 17 | 18 | class Server(tornado.web.RequestHandler): 19 | def initialize(self, mainMenu, token): 20 | self.mainMenu = mainMenu 21 | self.token = token 22 | 23 | def get(self): 24 | signature = self.get_argument("signature") 25 | timeStamp = self.get_argument("timestamp") 26 | nonce = self.get_argument("nonce") 27 | 28 | # 数据校验,微信接入 29 | if not self.checkSignature(signature, timeStamp, nonce): 30 | pass 31 | # log.warning("非法GET数据包,来自:" + self.request.remote_ip) 32 | else: 33 | echostr = self.get_argument("echostr", "default") 34 | if echostr != "default": 35 | self.write(echostr) 36 | # log.info("收到微信验证请求.") 37 | 38 | global maxWorks 39 | executor = ThreadPoolExecutor(maxWorks) # 并发数量 40 | @tornado.gen.coroutine 41 | def post(self): 42 | signature = self.get_argument("signature") 43 | timeStamp = self.get_argument("timestamp") 44 | nonce = self.get_argument("nonce") 45 | 46 | # 数据校验 47 | if not self.checkSignature(signature, timeStamp, nonce): 48 | # log.warning("非法POST数据包,来自:" + self.request.remote_ip) 49 | return 50 | 51 | # 处理数据 52 | rawXML = self.request.body.decode('utf-8') 53 | w = yield self.parser(rawXML) 54 | if w: 55 | self.write(w) 56 | 57 | @run_on_executor 58 | def parser(self, rawXML): 59 | tmp = parserRaw(rawXML, self.mainMenu) 60 | return tmp 61 | 62 | def checkSignature(self, signature, timeStamp, nonce): 63 | token = self.token 64 | tmp = [token, timeStamp, nonce] 65 | tmp.sort() 66 | raw = ''.join(tmp).encode() 67 | sha1Str = hashlib.sha1(raw).hexdigest() 68 | return sha1Str == signature 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /vlang/setup.py~: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | setup( 3 | name = "vlang", 4 | version = "0.0.3", 5 | py_modules = ['menu','message','parser','server','setup','user'], 6 | author = "sPeng", 7 | author_email = "ss@uutoto.com", 8 | description = "基于模型的微信开发框架,vlang" 9 | ) 10 | -------------------------------------------------------------------------------- /vlang/user.py: -------------------------------------------------------------------------------- 1 | import vlang.menu 2 | from builtins import isinstance 3 | 4 | class User(): 5 | def __init__(self, mainMenu): 6 | self.mainMenu = mainMenu 7 | self.currentMenu = mainMenu 8 | self.jump = True 9 | self.buffer = "" 10 | self.gen = None 11 | 12 | def handle(self, message): 13 | # 生成器 14 | if self.jump or self.gen == None: 15 | self.gen = self.currentMenu.action() 16 | self.new = True # 生成器是否未被使用 17 | self.jump = False 18 | 19 | # 运行生成器 20 | try: 21 | if self.new: 22 | value = self.gen.__next__() 23 | self.new = False 24 | else: 25 | value = self.gen.send(message["Content"]) 26 | 27 | except StopIteration: 28 | value = self.currentMenu # 生成器用完,默认从头开始 29 | 30 | # 所有的值都保存在list里 31 | if not isinstance(value, tuple): 32 | value = [value, ] 33 | else: 34 | value = list(value) 35 | 36 | valueType = list(value) # 深拷贝一份list版本 37 | 38 | # 如果返回值有Menu的实例,valueType中替换成py.menu.Menu 39 | # 如果返回值有Menu的子类,valueType中替换为py.menu.Menu 40 | # value中替换为Menu的实例 41 | # 如果返回值有str的实例,valueType中替换成str类 42 | for index, tmp in enumerate(valueType): 43 | if isinstance(tmp, vlang.menu.Menu): 44 | valueType[index] = vlang.menu.Menu 45 | continue 46 | 47 | if tmp.__class__ == type: 48 | if tmp.__bases__: 49 | if tmp.__bases__[0] == vlang.menu.Menu: 50 | valueType[index] = vlang.menu.Menu 51 | value[index] = self.mainMenu.getMenuTable()[value[index].__name__] 52 | # print("处理一个Menu子类返回情况") 53 | continue 54 | 55 | if isinstance(tmp, str): 56 | valueType[index] = str 57 | continue 58 | 59 | lastMenu = self.currentMenu 60 | # 判断返回类型 61 | if valueType == [str, vlang.menu.Menu, vlang.menu.Menu.BUFFER]: 62 | self.currentMenu = value[1] 63 | self.jump = True 64 | self.buffer += value[0] 65 | 66 | elif valueType == [str, ]: 67 | self.buffer += value[0] 68 | 69 | elif valueType == [str, vlang.menu.Menu]: 70 | self.currentMenu = value[1] 71 | self.jump = True 72 | self.buffer += value[0] 73 | 74 | elif valueType == [str, vlang.menu.Menu.BUFFER]: 75 | self.buffer += value[0] 76 | 77 | elif valueType == [vlang.menu.Menu, ]: 78 | self.currentMenu = value[0] 79 | self.jump = True 80 | return self.handle(message) # 下一个菜单的执行结果 81 | 82 | else: 83 | print("yield 格式错误") 84 | 85 | # 返回给上层的write() 86 | if str not in valueType: #没有回复文本 87 | return None 88 | 89 | # 有buffer的情况 90 | if vlang.menu.Menu.BUFFER in valueType: #缓存文本,不输出 91 | self.jump = False 92 | return self.handle(message) # 下一个菜单的执行结果 93 | 94 | # 填充发送方账号和接收方账号 95 | args = {} 96 | args["ToUserName"] = message["FromUserName"] 97 | args["FromUserName"] = message["ToUserName"] 98 | tmp = lastMenu.textMsg(self.buffer) 99 | tmp = tmp.format(**args) 100 | return tmp 101 | -------------------------------------------------------------------------------- /vlang/view.py: -------------------------------------------------------------------------------- 1 | from vlang.menu import Menu 2 | from vlang.server import start 3 | import time 4 | 5 | class MainMenu(Menu): 6 | def tag(self): 7 | self.name = "计算器" 8 | 9 | def action(self): 10 | reply = yield self.makeMenu() 11 | yield self.autoJump(reply) 12 | 13 | class Menu_1(Menu): 14 | def tag(self): 15 | self.name = "乘除法" 16 | 17 | def action(self): 18 | reply = yield self.makeMenu() 19 | yield self.autoJump(reply) 20 | 21 | class Menu_2(Menu): 22 | def tag(self): 23 | self.name = "表达式计算" 24 | 25 | def action(self): 26 | yield "这个功能还没开发呢!", mainMenu 27 | 28 | class Menu_1_1(Menu): 29 | def tag(self): 30 | self.name = "乘法" 31 | 32 | def action(self): 33 | try: 34 | reply = yield "请输入因数1:" 35 | num1 = float(reply) 36 | 37 | reply = yield "请输入因数2:" 38 | num2 = float(reply) 39 | time.sleep(4) 40 | num = num1 * num2 41 | yield "{0} x {1} = {2}\n".format(num1, num2, num) 42 | 43 | except ValueError: # 输入不是float型 44 | yield "输入错误,重新录入:\n", self.BUFFER 45 | 46 | class Menu_1_2(Menu): 47 | def tag(self): 48 | self.name = "除法" 49 | 50 | def action(self): 51 | yield "请输入被除数:" 52 | while(True): 53 | try: 54 | num1 = float(self.reply) 55 | yield "请输入除数:" 56 | num2 = float(self.reply) 57 | 58 | while(True): 59 | if num2 == 0: 60 | yield "除数不能为0,请重新输入:" 61 | num2 = float(self.reply) 62 | else: 63 | break 64 | 65 | break 66 | 67 | except ValueError: # 输入不是数字 68 | yield "输入不是数字,请重新输入被除数:" 69 | 70 | print("除法计算中") 71 | num3 = num1 / num2 72 | yield '''{0} ÷ {1} = {2} 73 | 退回主菜单输入0 74 | 继续乘法计算输入任意字符:'''.format(num1, num2, num3) 75 | if self.reply == "0": 76 | yield mainMenu 77 | 78 | if __name__ == "__main__": 79 | mainMenu = MainMenu() 80 | mainMenu.add(Menu_1) 81 | mainMenu.add(Menu_2) 82 | mainMenu.Menu_1.add(Menu_1_1) 83 | mainMenu.Menu_1.add(Menu_1_2) 84 | 85 | settings = {"url" :"/weixin", 86 | "token" : "CU342RIVA598BJ7OHEMGYDNWLKQT601Z", 87 | "port" : 8080 } 88 | start(mainMenu, settings) 89 | --------------------------------------------------------------------------------