diff --git a/README.MD b/README.MD index 0402142..88d1c43 100644 --- a/README.MD +++ b/README.MD @@ -66,9 +66,14 @@ QQBot 启动后,在另一个控制台窗口使用 qq 命令操作 QQBot ,目 4) 消息发送命令 qq send buddy|group|discuss $rinfo $message + + +    5) 消息接收命令 + +        qq poll - 5) 群管理命令: 设置/取消管理员 、 设置/删除群名片 、 群成员禁言 以及 踢除群成员 + 6) 群管理命令: 设置/取消管理员 、 设置/删除群名片 、 群成员禁言 以及 踢除群成员 qq group-set-admin $ginfo $minfo1,$minfo2,... @@ -83,7 +88,7 @@ QQBot 启动后,在另一个控制台窗口使用 qq 命令操作 QQBot ,目 qq group-kick $ginfo $minfo1,$minfo2,... - 6) 加载/卸载/显示插件 + 7) 加载/卸载/显示插件 qq plug/unplug myplugin @@ -458,6 +463,9 @@ GUI 模式是默认的模式,只适用于个人电脑。邮箱模式可以适 # 二维码 http 服务器端口号 "httpServerPort" : 8189, + + # Tornado restapi 服务器端口号 + "restapiServerPort" : 8190, # 自动登录的 QQ 号 "qq" : "3497303033", @@ -512,6 +520,7 @@ GUI 模式是默认的模式,只适用于个人电脑。邮箱模式可以适 # "termServerPort" : 8188, # "httpServerIP" : "", # "httpServerPort" : 8189, + # "restapiServerPort" : 8190, # "qq" : "", # "mailAccount" : "", # "mailAuthCode" : "", @@ -580,6 +589,10 @@ QQBot 启动后,会开启一个 QQBot-term 服务器监听用户通过 qq 命 如果需要在同一台机器上登录多个 QQ 号码,可以直接在不同的终端中开启多个 qqbot 进程进行登录,但是,每个 qqbot 进程必须设置专有的 termServerPort 和 httpServerPort (或者全部设置为 0 或 空值 ),否则会造成端口号冲突。 +#### QQBot-restapi 服务器端口号( restapiServerPort ) + +逻辑和 QQBot-term 一样,但是是基于Tornado的一个restapi接口。便于使用其它程序和QQBot进行交互 + #### 调试模式( debug ) 若 debug 项设置为 True ,则运行过程中会打印调试信息。 diff --git a/faq.md b/faq.md index 77a2a65..27b0927 100644 --- a/faq.md +++ b/faq.md @@ -54,7 +54,7 @@ contact.ctype 为 'buddy'/'group'/'discuss' 时,分别代表本消息时 好 #### 如何采用 virtualenv 将本项目安装至独立的运行环境? -本项目依赖于 reqests 、flask 、 certifi 和 apscheduler 库,用 pip 安装本项目时会自动安装以上四个库以及它们所依赖的库。一般来说安装本项目不会与系统其他项目冲突,因此可直接安装至系统的全局 site-packages 目录。 +本项目依赖于 reqests 、flask 、 certifi 、 tornado 和 apscheduler 库,用 pip 安装本项目时会自动安装以上四个库以及它们所依赖的库。一般来说安装本项目不会与系统其他项目冲突,因此可直接安装至系统的全局 site-packages 目录。 在某些系统中可能会出现 https 请求错误,这时需要安装 certifi 库的指定版本(2015.4.28 版),可能会将系统中已有的 certifi 库升级或降级并导致会使系统中的其他项目无法使用,这时可以使用 virtualenv 将本项目安装至独立的运行环境中。 @@ -76,6 +76,7 @@ virtualenv 基本原理和使用可参考 [廖雪峰的教程](http://www.liaoxu pip install certifi==2015.4.28 pip install flask==0.12 pip install apscheduler==3.3.1 + pip install tornado==4.5.2 pip install qqbot 注意:使用本方式安装本项目后,每次使用 qqbot 和 qq 命令之前,需要先运行下面这条命令激活 qqbot-venv 下的运行环境: @@ -97,6 +98,7 @@ Windows 下, 上述脚本改为: pip install certifi==2015.4.28 pip install flask==0.12 pip install apscheduler==3.3.1 + pip install tornado==4.5.2 pip install qqbot 其中 %UserProfile% 是用户主目录,Win7中为 C:\Users\xxx 目录。 diff --git a/qqbot/qconf.py b/qqbot/qconf.py index 83a14a6..8f11094 100644 --- a/qqbot/qconf.py +++ b/qqbot/qconf.py @@ -28,6 +28,9 @@ # 二维码 http 服务器端口号 "httpServerPort" : 8189, + + # restapi 服务器端口号 + "restapiServerPort" : 8190, # 自动登录的 QQ 号 "qq" : "3497303033", @@ -82,6 +85,7 @@ # "termServerPort" : 8188, # "httpServerIP" : "", # "httpServerPort" : 8189, + # "restapiServerPort" : 8190, # "qq" : "", # "mailAccount" : "", # "mailAuthCode" : "", @@ -102,6 +106,7 @@ "termServerPort" : 8188, "httpServerIP" : "", "httpServerPort" : 8189, + "restapiServerPort" : 8190, "qq" : "", "mailAccount" : "", "mailAuthCode" : "", @@ -124,7 +129,7 @@ QQBot 机器人 用法: {PROGNAME} [-h] [-d] [-nd] [-u USER] [-q QQ] - [-p TERMSERVERPORT] [-ip HTTPSERVERIP][-hp HTTPSERVERPORT] + [-p TERMSERVERPORT] [-rp RESTAPISERVERPORT] [-ip HTTPSERVERIP][-hp HTTPSERVERPORT] [-m MAILACCOUNT] [-mc MAILAUTHCODE] [-r] [-nr] [-fi FETCHINTERVAL] @@ -156,6 +161,9 @@ -p TERMSERVERPORT, --termServerPort TERMSERVERPORT 更改QTerm控制台的监听端口到 TERMSERVERPORT 。 默认的监听端口是 8188 (TCP)。 + -rp RESTAPISERVERPORT, --restapiServerPort RESTAPISERVERPORT + 更改RestAPI监听端口到 RESTAPISERVERPORT 。 + 默认的监听端口是 8190 (TCP)。 HTTP二维码查看服务器设置: (请阅读说明文件以了解此HTTP服务器的详细信息。) @@ -227,7 +235,9 @@ def readCmdLine(self, argv): parser.add_argument('-ip', '--httpServerIP') - parser.add_argument('-hp', '--httpServerPort', type=int) + parser.add_argument('-hp', '--httpServerPort', type=int) + + parser.add_argument('-rp', '--restapiServerPort', type=int) parser.add_argument('-m', '--mailAccount') @@ -427,6 +437,7 @@ def Display(self): INFO('二维码服务器 ip :%s', self.httpServerIP or '无') INFO('二维码服务器端口号:%s', self.httpServerIP and self.httpServerPort or '无') + INFO('RestAPI服务器端口号:%s', self.restapiServerPort or '无') INFO('用于接收二维码的邮箱账号:%s', self.mailAccount or '无') INFO('邮箱服务授权码:%s', self.mailAccount and '******' or '无') INFO('以文本模式显示二维码:%s', self.cmdQrcode and '是' or '否') diff --git a/qqbot/qqbotcls.py b/qqbot/qqbotcls.py index 1882936..9eb9a32 100644 --- a/qqbot/qqbotcls.py +++ b/qqbot/qqbotcls.py @@ -20,8 +20,9 @@ from qqbot.qconf import QConf from qqbot.utf8logger import INFO, CRITICAL, ERROR, WARN from qqbot.qsession import QLogin, RequestError -from qqbot.common import StartDaemonThread, Import +from qqbot.common import StartDaemonThread, Import, Queue from qqbot.qterm import QTermServer +from qqbot.webserver.server import TornadoServer from qqbot.mainloop import MainLoop, Put from qqbot.groupmanager import GroupManager from qqbot.termbot import TermBot @@ -128,6 +129,37 @@ def Login(self, argv=None): # child thread 1 self.poll = session.Copy().Poll + def PollMessage(self): + maxIter = 1024 + result = [] + while maxIter > 0: + try: + if not self.msgQueue.empty(): + tmp = self.msgQueue.get(block=False) + if tmp[1] is None: + tmpdict = { + 'ctype' : tmp[5], + 'buddy' : str(tmp[0]), + 'content' : tmp[2], + 'fromUin' : tmp[3], + } + else: + tmpdict = { + 'ctype' : tmp[5], + 'group' : str(tmp[0]), + 'member' : str(tmp[1]), + 'content' : tmp[2], + 'fromUin' : tmp[3], + 'membUin' : tmp[4], + } + result.append(tmpdict) + maxIter -= 1 + else: + break + except Exception as e: + ERROR('处理消息队列出错 %s'%str(e), exc_info=True) + return result + def Run(self): if self.conf.startAfterFetch: self.firstFetch() @@ -139,6 +171,7 @@ def Run(self): StartDaemonThread(self.pollForever) StartDaemonThread(self.intervalForever) StartDaemonThread(QTermServer(self.conf.termServerPort, self.onTermCommand).Run) + StartDaemonThread(TornadoServer(self.conf.restapiServerPort, self.onCommand).Run) self.scheduler.start() self.started = True @@ -198,6 +231,18 @@ def onPollComplete(self, ctype, fromUin, membUin, content): else: INFO('来自 %s[%s] 的消息: "%s"' % (contact, member, content)) + try: + self.msgQueue.put((contact, member, content, fromUin, membUin, ctype), block=False) + except queue.Full: + self.discardedMessage += 1 + self.msgQueue.get(block=False) + try: + self.msgQueue.put((contact, member, content, fromUin, membUin, ctype), block=False) + except Exception as e: + ERROR('消息队列出错 %s'%str(e), exc_info=True) + except Exception as e: + ERROR('消息队列出错 %s'%str(e), exc_info=True) + self.onQQMessage(contact, member, content) def detectAtMe(self, nameInGroup, content): @@ -240,6 +285,9 @@ def init(self, argv): for pluginName in self.conf.plugins: self.Plug(pluginName) + self.discardedMessage = 0 + self.msgQueue = Queue.Queue(maxsize=1024) + self.onInit() def wrap(self, slots): diff --git a/qqbot/termbot.py b/qqbot/termbot.py index 13cedee..b92de61 100644 --- a/qqbot/termbot.py +++ b/qqbot/termbot.py @@ -4,10 +4,40 @@ from qqbot.mainloop import Put from qqbot.common import Unquote, STR2BYTES, JsonDumps, BYTES2STR +import pdb + cmdFuncs, usage = {}, {} class TermBot(object): + def onCommandDict(bot, argv, http): + if argv and 'cmd' in argv and argv['cmd'] in cmdFuncs: + try: + cmd = argv['cmd'] + argv.pop('cmd', None) + if argv['subcmd'] == None: + argv.pop('subcmd', None) + result, err = cmdFuncs[cmd](bot, argv, http) + except Exception as e: + result, err = None, '运行命令过程中出错:' + str(type(e)) + str(e) + ERROR(err, exc_info=True) + else: + result, err = None, 'QQBot 命令格式错误' + return result, err + + def onCommand(bot, argv, http=True): + if isinstance(argv, dict): + return TermBot.onCommandDict(bot, argv, http) + if argv and argv[0] in cmdFuncs: + try: + result, err = cmdFuncs[argv[0]](bot, argv[1:], http) + except Exception as e: + result, err = None, '运行命令过程中出错:' + str(type(e)) + str(e) + ERROR(err, exc_info=True) + else: + result, err = None, 'QQBot 命令格式错误' + return result, err + def onTermCommand(bot, command): command = BYTES2STR(command) if command.startswith('GET /'): @@ -24,14 +54,7 @@ def onTermCommand(bot, command): http = False argv = command.strip().split(None, 3) - if argv and argv[0] in cmdFuncs: - try: - result, err = cmdFuncs[argv[0]](bot, argv[1:], http) - except Exception as e: - result, err = None, '运行命令过程中出错:' + str(type(e)) + str(e) - ERROR(err, exc_info=True) - else: - result, err = None, 'QQBot 命令格式错误' + result, err = TermBot.onCommand(bot, argv, http) if http: rep = {'result':result, 'err': err} @@ -48,14 +71,16 @@ def onTermCommand(bot, command): def cmd_help(bot, args, http=False): '''1 help''' - if len(args) == 0: + if isinstance(args, dict) and dict == {}: + return usage['term'], None + elif len(args) == 0: return usage['term'], None else: return None, 'QQBot 命令格式错误' def cmd_stop(bot, args, http=False): '''1 stop''' - if len(args) == 0: + if (isinstance(args, dict) and dict == {}) or len(args) == 0: Put(bot.Stop) return 'QQBot已停止', None else: @@ -63,7 +88,7 @@ def cmd_stop(bot, args, http=False): def cmd_restart(bot, args, http=False): '''1 restart''' - if len(args) == 0: + if (isinstance(args, dict) and dict == {}) or len(args) == 0: Put(bot.Restart) return 'QQBot已重启(自动登录)', None else: @@ -71,7 +96,7 @@ def cmd_restart(bot, args, http=False): def cmd_fresh_restart(bot, args, http=False): '''1 fresh-restart''' - if len(args) == 0: + if (isinstance(args, dict) and dict == {}) or len(args) == 0: Put(bot.FreshRestart) return 'QQBot已重启(手工登录)', None else: @@ -80,23 +105,56 @@ def cmd_fresh_restart(bot, args, http=False): def cmd_list(bot, args, http=False): '''2 list buddy|group|discuss [qq|name|key=val] 2 list group-member|discuss-member oqq|oname|okey=oval [qq|name|key=val]''' - - if (len(args) in (1, 2)) and args[0] in ('buddy', 'group', 'discuss'): + if isinstance(args, dict) and 'subcmd' in args.keys(): + if args['subcmd'] in ('buddy', 'group', 'discuss'): + tmp = [args['subcmd']] + args.pop('subcmd', None) + if len(args.keys()) == 1: + key = list(args.keys())[0] + if key == 'name' or key == 'qq': + tmp.append(args[key]) + else: + tmp.append('%s=%s'%(key, args[key])) + return bot.ObjOfList(*tmp) + elif args['subcmd'] in ('group-member', 'discuss-member'): + tmp = [args['subcmd']] + args.pop('subcmd', None) + if len(args.keys()) == 1: + key = list(args.keys())[0] + if key == 'oname' or key == 'oqq': + tmp.append(args[key]) + else: + tmp.append('%s=%s'%(key[1:], args[key])) + elif len(args.keys()) == 2: + for key in args.keys(): + if key[0] == 'o': + if key == 'oname' or key == 'oqq': + tmp.append(args[key]) + else: + tmp.append('%s=%s'%(key[1:], args[key])) + else: + if key == 'name' or key == 'qq': + tmp.append(args[key]) + else: + tmp.append('%s=%s'%(key, args[key])) + else: + return None, 'QQBot 命令格式错误' + return bot.ObjOfList(*tmp) + return None, 'QQBot 命令格式错误' + elif isinstance(args, list) and (len(args) in (1, 2)) and args[0] in ('buddy', 'group', 'discuss'): # list buddy # list buddy jack if not http: return bot.StrOfList(*args), None else: - return bot.ObjOfList(*args) - - elif (len(args) in (2, 3)) and args[1] and (args[0] in ('group-member', 'discuss-member')): + return bot.ObjOfList(*args) + elif isinstance(args, list) and (len(args) in (2, 3)) and args[1] and (args[0] in ('group-member', 'discuss-member')): # list group-member xxx班 # list group-member xxx班 yyy if not http: return bot.StrOfList(*args), None else: return bot.ObjOfList(*args) - else: return None, 'QQBot 命令格式错误' @@ -104,10 +162,37 @@ def cmd_update(bot, args, http=False): '''2 update buddy|group|discuss 2 update group-member|discuss-member oqq|oname|okey=oval''' - if len(args) == 1 and args[0] in ('buddy', 'group', 'discuss'): + if isinstance(args, dict) and 'subcmd' in args.keys(): + if args['subcmd'] in ('buddy', 'group', 'discuss'): + tmp = [args['subcmd']] + args.pop('subcmd', None) + if args == {}: + return bot.Update(tmp[0]), None + return None, 'QQBot 命令格式错误' + elif args['subcmd'] in ('group-member', 'discuss-member'): + tmp = [args['subcmd']] + args.pop('subcmd', None) + if len(args.keys()) == 1: + key = list(args.keys())[0] + if key == 'oqq' or key == 'oname': + tmp.append(args[key]) + else: + tmp.append('%s=%s'%(key[1:], args[key])) + else: + return None, 'QQBot 命令格式错误' + cl = bot.List(tmp[0][:-7], tmp[1]) + if cl is None: + return None, 'QQBot 在向 QQ 服务器请求数据获取联系人资料的过程中发生错误' + elif not cl: + return None, '%s-%s 不存在' % (tmp[0], tmp[1]) + else: + return [bot.Update(c) for c in cl], None + else: + return None, 'QQBot 命令格式错误' + elif isinstance(args, list) and len(args) == 1 and args[0] in ('buddy', 'group', 'discuss'): # update buddy - return bot.Update(args[0]), None - elif len(args) == 2 and args[1] and (args[0] in ('group-member', 'discuss-member')): + return bot.Update(args[0]), None + elif isinstance(args, list) and len(args) == 2 and args[1] and (args[0] in ('group-member', 'discuss-member')): # update group-member xxx班 cl = bot.List(args[0][:-7], args[1]) if cl is None: @@ -121,8 +206,32 @@ def cmd_update(bot, args, http=False): def cmd_send(bot, args, http=False): '''3 send buddy|group|discuss qq|name|key=val message''' - - if len(args) == 3 and args[0] in ('buddy', 'group', 'discuss'): + if isinstance(args, dict): + tmp = [args['subcmd']] + msg = None + args.pop('subcmd', None) + if len(args.keys()) == 2: + for key in args.keys(): + if key == 'name' or key == 'qq': + tmp.append(args[key]) + elif key == 'message': + msg = args[key] + else: + tmp.append('%s=%s'%(key, args[key])) + if msg is not None: + cl = bot.List(tmp[0], tmp[1]) + if cl is None: + return None, 'QQBot 在向 QQ 服务器请求数据获取联系人资料的过程中发生错误' + elif not cl: + return None, '%s-%s 不存在' % (tmp[0], tmp[1]) + else: + msg = msg.replace('\\n','\n').replace('\\t','\t') + result = [bot.SendTo(c, msg) for c in cl] + if not http: + result = '\n'.join(result) + return result, None + return None, 'QQBot 命令格式错误' + elif isinstance(args, list) and len(args) == 3 and args[0] in ('buddy', 'group', 'discuss'): # send buddy jack hello cl = bot.List(args[0], args[1]) if cl is None: @@ -138,6 +247,18 @@ def cmd_send(bot, args, http=False): else: return None, 'QQBot 命令格式错误' +def cmd_poll(bot, args, http=False): + '''1 poll''' + if isinstance(args, dict): + args.pop('subcmd', None) + result = bot.PollMessage() + return result, None + elif isinstance(args, list) and len(args) == 0: + result = bot.PollMessage() + return result, None + else: + return None, 'QQBot 命令格式错误' + def group_operation(bot, ginfo, minfos, func, exArgs, http): gl = bot.List('group', ginfo) if gl is None: @@ -233,21 +354,30 @@ def cmd_group_unset_card(bot, args, http=False): def cmd_plug(bot, args, http=False): '''5 plug myplugin''' - if len(args) == 1: + if isinstance(args, dict) and 'name' in args and len(args.keys()) == 1: + return bot.Plug(args['name']), None + elif isinstance(args, list) and len(args) == 1: return bot.Plug(args[0]), None else: return None, 'QQBot 命令格式错误' def cmd_unplug(bot, args, http=False): '''5 unplug myplugin''' - if len(args) == 1: + if isinstance(args, dict) and 'name' in args and len(args.keys()) == 1: + return bot.Unplug(args['name']), None + elif isinstance(args, list) and len(args) == 1: return bot.Unplug(args[0]), None else: return None, 'QQBot 命令格式错误' def cmd_plugins(bot, args, http=False): '''5 plugins''' - if len(args) == 0: + if isinstance(args, dict) and args == {}: + if not http: + return '已加载插件:%s' % bot.Plugins(), None + else: + return bot.Plugins(), None + elif isinstance(args, list) and len(args) == 0: if not http: return '已加载插件:%s' % bot.Plugins(), None else: @@ -275,7 +405,10 @@ def cmd_plugins(bot, args, http=False): 4) 消息发送命令 qq send buddy|group|discuss qq|name|key=val message -5) 群管理命令: 设置/取消管理员 、 设置/删除群名片 、 群成员禁言 以及 踢除群成员 +5) 消息接收命令 + qq poll + +6) 群管理命令: 设置/取消管理员 、 设置/删除群名片 、 群成员禁言 以及 踢除群成员 qq group-set-admin ginfo minfo1,minfo2,... qq group-unset-admin ginfo minfo1,minfo2,... qq group-set-card ginfo minfo1,minfo2,... card @@ -283,7 +416,7 @@ def cmd_plugins(bot, args, http=False): qq group-shut ginfo minfo1,minfo2,... [t] qq group-kick ginfo minfo1,minfo2,... -6) 加载/卸载/显示插件 +7) 加载/卸载/显示插件 qq plug/unplug myplugin qq plugins\ ''' diff --git a/qqbot/webserver/__init__.py b/qqbot/webserver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qqbot/webserver/server.py b/qqbot/webserver/server.py new file mode 100644 index 0000000..d8bff54 --- /dev/null +++ b/qqbot/webserver/server.py @@ -0,0 +1,71 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +QQBot -- A conversation robot base on Tencent's SmartQQ +Website -- https://github.com/wilsonwang371/qqbot +Author -- wilsonw@vmware.com + +This is a new web server that based on Tornado web server +""" +import sys, os +p = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if p not in sys.path: + sys.path.insert(0, p) + +import tornado.ioloop +import tornado.web +from qqbot.common import STR2BYTES, JsonDumps +from qqbot.mainloop import Put +from qqbot.utf8logger import ERROR, INFO +from tornado.web import asynchronous + + + +HOST, DEFPORT = '127.0.0.1', 8189 + +class MainHandler(tornado.web.RequestHandler): + def initialize(self, server): + self.server = server + + @asynchronous + def get(self, cmd, subcmd=None): + data_dict = { + 'cmd': cmd, + 'subcmd': subcmd, + } + for i in self.request.arguments: + data_dict[i] = self.get_argument(i, None) + Put(self.onData, data_dict, self.server) + + def onData(self, data_dict, server): + try: + result, err = server.response(data_dict) + rep = {'result':result, 'err': err} + resp = STR2BYTES(JsonDumps(rep, ensure_ascii=False, indent=4)) + except Exception as e: + resp = '在处理 请求时发生错误,%s' % (e) + ERROR(resp, exc_info = True) + resp = STR2BYTES(resp) + self.write(resp) + self.finish() + + +class TornadoServer(object): + + def __init__(self, port, onCommand): + self.response = onCommand + self.host = HOST + self.port = int(port) + self.name = 'QQBot-Term 服务器' + + def Run(self): + if not self.port: + INFO('QQBot-RestAPI 服务器未开启') + else: + self.app = tornado.web.Application([ + (r'/api/([^/]+)', MainHandler, dict(server=self)), + (r'/api/([^/]+)/([^/]+)', MainHandler, dict(server=self)), + ]) + self.app.listen(self.port) + tornado.ioloop.IOLoop.current().start() diff --git a/requirements.txt b/requirements.txt index c5e74e0..a88e0d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ requests==2.7.0 certifi==2015.4.28 flask==0.12 apscheduler==3.3.1 +tornado==4.5.2 diff --git a/setup.py b/setup.py index 3b43e8f..de1c4ce 100644 --- a/setup.py +++ b/setup.py @@ -7,14 +7,14 @@ setup( name = 'qqbot', version = version, - packages = ['qqbot', 'qqbot.qcontactdb', 'qqbot.plugins'], + packages = ['qqbot', 'qqbot.qcontactdb', 'qqbot.plugins', 'qqbot.webserver'], entry_points = { 'console_scripts': [ 'qqbot = qqbot:RunBot', 'qq = qqbot:QTerm' ] }, - install_requires = ['requests', 'certifi', 'apscheduler'], + install_requires = ['requests', 'certifi', 'apscheduler', 'tornado'], description = "QQBot: A conversation robot base on Tencent's SmartQQ", author = 'pandolia' , author_email = 'pandolia@yeah.net',