├── plugins ├── test.py ├── url_reader.py ├── lisp.py ├── _pinyin.py ├── pyshell.py ├── paste.py ├── weather.py ├── config.py ├── config.py.bak1 ├── shell.py ├── translate.py ├── command.py ├── douban.py ├── lang.py ├── pm25.py ├── simsimi.py ├── __init__.py ├── _linktitle.py ├── smartrobot.py └── _fetchtitle.py ├── README.md ├── run.cmd ├── run_qq.sh ├── setup.py ├── twqq ├── __init__.py ├── const.py ├── _hash.py ├── client.py ├── tornadohttpclient.py ├── objects.py ├── hub.py └── requests.py ├── .gitignore ├── LICENSE ├── robot.py ├── server.py └── webqq.py /plugins/test.py: -------------------------------------------------------------------------------- 1 | aaaa 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | robot 2 | ===== 3 | 4 | robot机器人 5 | -------------------------------------------------------------------------------- /run.cmd: -------------------------------------------------------------------------------- 1 | set PYTHONPATH=src;qq;plugins 2 | C:\Python27\python webqq.py -------------------------------------------------------------------------------- /run_qq.sh: -------------------------------------------------------------------------------- 1 | export PYTHONPATH=twqq:plugins 2 | python webqq.py 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # setup.py 2 | from distutils.core import setup 3 | import py2exe 4 | 5 | setup(console=["webqq.py"]) -------------------------------------------------------------------------------- /twqq/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 13/11/15 10:48:17 7 | # Desc : 8 | # 9 | 10 | __version__ = '0.2.11' 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | -------------------------------------------------------------------------------- /plugins/url_reader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 14/01/16 12:00:06 7 | # Desc : 读取URL信息(标题)插件 8 | # 9 | import re 10 | 11 | from plugins import BasePlugin 12 | 13 | from _linktitle import get_urls, fetchtitle 14 | 15 | class URLReaderPlugin(BasePlugin): 16 | URL_RE = re.compile(r"(http[s]?://(?:[-a-zA-Z0-9_]+\.)+[a-zA-Z]+(?::\d+)" 17 | "?(?:/[-a-zA-Z0-9_%./]+)*\??[-a-zA-Z0-9_&%=.]*)", 18 | re.UNICODE) 19 | 20 | def is_match(self, from_uin, content, type): 21 | urls = get_urls(content) 22 | if urls: 23 | self._urls = urls 24 | return True 25 | return False 26 | 27 | def handle_message(self, callback): 28 | fetchtitle(self._urls, callback) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 evilbinary 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /plugins/lisp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 14/01/22 14:13:23 7 | # Desc : 调用接口实现运行Lisp程序 8 | # 9 | import re 10 | import logging 11 | 12 | from plugins import BasePlugin 13 | 14 | logger = logging.getLogger("plugin") 15 | 16 | class LispPlugin(BasePlugin): 17 | url = "http://www.compileonline.com/execute_new.php" 18 | result_p = re.compile(r'
(.*?)
', flags = re.U|re.M|re.S) 19 | 20 | def is_match(self, from_uin, content, type): 21 | if content.startswith("(") and content.endswith(")"): 22 | self._code = content 23 | return True 24 | return False 25 | 26 | def handle_message(self, callback): 27 | params = {"args":"", "code":self._code.encode("utf-8"), 28 | "inputs":"", "lang":"lisp", "stdinput":""} 29 | def read(resp): 30 | logger.info(u"Lisp request success, result: {0}".format(resp.body)) 31 | result = self.result_p.findall(resp.body) 32 | result = "" if not result else result[0] 33 | #callback(result) 34 | self.http.post(self.url, params, callback = read) 35 | -------------------------------------------------------------------------------- /plugins/_pinyin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | Author:cleverdeng 6 | E-mail:clverdeng@gmail.com 7 | """ 8 | 9 | __version__ = '0.9' 10 | __all__ = ["PinYin"] 11 | 12 | import os.path 13 | 14 | 15 | class PinYin(object): 16 | def __init__(self, dict_file='plugins/word.data'): 17 | self.word_dict = {} 18 | self.dict_file = dict_file 19 | 20 | 21 | def load_word(self): 22 | if not os.path.exists(self.dict_file): 23 | raise IOError("NotFoundFile") 24 | 25 | with file(self.dict_file) as f_obj: 26 | for f_line in f_obj.readlines(): 27 | try: 28 | line = f_line.split(' ') 29 | self.word_dict[line[0]] = line[1] 30 | except: 31 | line = f_line.split(' ') 32 | self.word_dict[line[0]] = line[1] 33 | 34 | 35 | def hanzi2pinyin(self, string=""): 36 | result = [] 37 | if not isinstance(string, unicode): 38 | string = string.decode("utf-8") 39 | 40 | for char in string: 41 | key = '%X' % ord(char) 42 | result.append(self.word_dict.get(key, char).split()[0][:-1].lower()) 43 | 44 | return result 45 | 46 | 47 | def hanzi2pinyin_split(self, string="", split=""): 48 | result = self.hanzi2pinyin(string=string) 49 | if split == "": 50 | return result 51 | else: 52 | return split.join(result) 53 | 54 | 55 | if __name__ == "__main__": 56 | test = PinYin() 57 | test.load_word() 58 | string = "钓鱼岛是中国的" 59 | print "in: %s" % string 60 | print "out: %s" % str(test.hanzi2pinyin(string=string)) 61 | print "out: %s" % test.hanzi2pinyin_split(string=string, split="-") 62 | -------------------------------------------------------------------------------- /twqq/const.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 13/11/12 10:03:49 7 | # Desc : 8 | # 9 | 10 | CHECK_REFERER = "https://ui.ptlogin2.qq.com/cgi-bin/login?daid="\ 11 | "164&target=self&style=5&mibao_css=m_webqq&appid=1003903&"\ 12 | "enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb2.q"\ 13 | "q.com%2Floginproxy.html&f_url=loginerroralert&strong_log"\ 14 | "in=1&login_state=10&t=20130723001" 15 | 16 | CHECK_U1 = "http://web2.qq.com/loginproxy.html" 17 | 18 | BLOGIN_U1 = "http://www.qq.com/loginproxy.html?login2qq=1&webqq_type=10" 19 | 20 | BLOGIN_REFERER = "https://ui.ptlogin2.qq.com/cgi-bin/login?target=self&style=5&mibao_css=mwebqq&appid=1003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb.qq.com%2Floginproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20130221001" 21 | BLOGIN_R_REFERER = "https://ui.ptlogin2.qq.com/cgi-bin/login?daid=164&target=self&style=5&mibao_css=m_webqq&appid=1003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb2.qq.com%2Floginproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20130903001" 22 | LOGIN_REFERER = "https://ui.ptlogin2.qq.com/cgi-bin/login?daid=164&target=self&style=5&mibao_css=m_webqq&appid=1003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb2.qq.com%2Floginproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20130723001" 23 | 24 | S_REFERER = "http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=3" 25 | S_ORIGIN = "http://s.web2.qq.com" 26 | 27 | D_REFERER = "https://d.web2.qq.com/cfproxy.html?v=20110331002&callback=1" 28 | D_ORIGIN = "http://d.web2.qq.com" 29 | 30 | DEFAULT_STYLE = {"name": "Monospace", "size": 10, 31 | "style": [0, 0, 0], "color":"000000"} 32 | -------------------------------------------------------------------------------- /plugins/pyshell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 14/01/16 12:29:39 7 | # Desc : Python 在线 Shell 插件 8 | # 9 | import config 10 | 11 | from plugins.paste import PastePlugin 12 | 13 | class PythonShellPlugin(PastePlugin): 14 | def is_match(self, from_uin, content, type): 15 | if content.startswith(">>>"): 16 | body = content.lstrip(">").lstrip(" ") 17 | bodys = [] 18 | for b in body.replace("\r\n", "\n").split("\n"): 19 | bodys.append(b.lstrip(">>>")) 20 | self.body = "\n".join(bodys) 21 | self.from_uin = from_uin 22 | return True 23 | return False 24 | 25 | def handle_message(self, callback): 26 | self.shell(callback) 27 | 28 | def shell(self, callback): 29 | """ 实现Python Shell 30 | Arguments: 31 | `callback` - 发送结果的回调 32 | """ 33 | if self.body.strip() in ["cls", "clear"]: 34 | url = "http://pythonec.appspot.com/drop" 35 | params = [("session", self.from_uin),] 36 | else: 37 | url = "http://pythonec.appspot.com/shell" 38 | #url = "http://localhost:8080/shell" 39 | params = [("session", self.from_uin), 40 | ("statement", self.body.encode("utf-8"))] 41 | 42 | def read_shell(resp): 43 | data = resp.body 44 | if not data: 45 | data = "OK" 46 | if len(data) > config.MAX_LENGTH: 47 | return self.paste(data, callback, "") 48 | 49 | if data.count("\n") > 10: 50 | data.replace("\n", " ") 51 | 52 | callback(data.decode("utf-8")) 53 | return 54 | 55 | self.http.get(url, params, callback = read_shell) 56 | -------------------------------------------------------------------------------- /plugins/paste.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 14/01/16 12:13:09 7 | # Desc : 粘贴代码插件 8 | # 9 | from plugins import BasePlugin 10 | 11 | class PastePlugin(BasePlugin): 12 | code_typs = ['actionscript', 'ada', 'apache', 'bash', 'c', 'c#', 'cpp', 13 | 'css', 'django', 'erlang', 'go', 'html', 'java', 'javascript', 14 | 'jsp', 'lighttpd', 'lua', 'matlab', 'mysql', 'nginx', 15 | 'objectivec', 'perl', 'php', 'python', 'python3', 'ruby', 16 | 'scheme', 'smalltalk', 'smarty', 'sql', 'sqlite3', 'squid', 17 | 'tcl', 'text', 'vb.net', 'vim', 'xml', 'yaml'] 18 | 19 | def is_match(self, from_uin, content, type): 20 | if content.startswith("```"): 21 | typ = content.split("\n")[0].lstrip("`").strip().lower() 22 | self.ctype = typ if typ in self.code_typs else "text" 23 | self.code = "\n".join(content.split("\n")[1:]) 24 | return True 25 | return False 26 | 27 | def paste(self, code, callback, ctype = "text"): 28 | """ 贴代码 """ 29 | params = {'vimcn':code.encode("utf-8")} 30 | url = "http://p.vim-cn.com/" 31 | 32 | self.http.post(url, params, callback = self.read_paste, 33 | kwargs = {"callback":callback, "ctype":ctype}) 34 | 35 | 36 | def read_paste(self, resp, callback, ctype="text"): 37 | """ 读取贴代码结果, 并发送消息 """ 38 | if resp.code == 200: 39 | content = resp.body.strip().rstrip("/") + "/" + ctype 40 | elif resp.code == 400: 41 | content = u"内容太短, 不需要贴!" 42 | else: 43 | content = u"没贴上, 我也不知道为什么!" 44 | 45 | callback(content) 46 | 47 | 48 | def handle_message(self, callback): 49 | self.paste(self.code, callback, self.ctype) 50 | -------------------------------------------------------------------------------- /plugins/weather.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 14/01/16 15:59:20 7 | # Desc : 天气 8 | # 9 | """ 代码贡献自 EricTang (汤勺), 由 cold 整理 10 | """ 11 | 12 | from xml.dom.minidom import parseString 13 | 14 | from plugins import BasePlugin 15 | 16 | #weather service url address 17 | WEATHER_URL = 'http://www.webxml.com.cn/webservices/weatherwebservice.asmx/getWeatherbyCityName' 18 | 19 | class WeatherPlugin(BasePlugin): 20 | def is_match(self, from_uin, content, type): 21 | if content.startswith("-w"): 22 | self.city = content.split(" ")[1] 23 | self._format = u"\n {0}" if type == "g" else u"{0}" 24 | return True 25 | return False 26 | 27 | 28 | def handle_message(self, callback): 29 | self.get_weather(self.city, callback) 30 | 31 | def get_weather(self, city, callback): 32 | """ 33 | 根据城市获取天气 34 | """ 35 | if city: 36 | params = {"theCityName":city.encode("utf-8")} 37 | self.http.get(WEATHER_URL, params, callback = self.callback, 38 | kwargs = {"callback":callback}) 39 | else: 40 | callback(self._format.foramt(u"缺少城市参数")) 41 | 42 | def callback(self, resp, callback): 43 | #解析body体 44 | document = "" 45 | for line in resp.body.split("\n"): 46 | document = document + line 47 | 48 | dom = parseString(document) 49 | 50 | strings = dom.getElementsByTagName("string") 51 | 52 | temperature_of_today = self.getText(strings[5].childNodes) 53 | weather_of_today = self.getText(strings[6].childNodes) 54 | 55 | temperature_of_tomorrow = self.getText(strings[12].childNodes) 56 | weather_of_tomorrow = self.getText(strings[13].childNodes) 57 | 58 | weatherStr = u"今明两天%s的天气状况是: %s %s ; %s %s;" % \ 59 | (self.city, weather_of_today, temperature_of_today, 60 | weather_of_tomorrow, temperature_of_tomorrow) 61 | 62 | callback(self._format.format(weatherStr)) 63 | 64 | 65 | def getText(self, nodelist): 66 | """ 67 | 获取所有的string字符串string标签对应的文字 68 | """ 69 | rc = "" 70 | for node in nodelist: 71 | if node.nodeType == node.TEXT_NODE: 72 | rc = rc + node.data 73 | return rc 74 | 75 | -------------------------------------------------------------------------------- /plugins/config.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # 3 | # Copyright 2013 cold 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # 是否输出调试信息 19 | DEBUG = True 20 | 21 | # 是否显示详细HTTP跟踪信息 22 | TRACE = False 23 | 24 | # QQ 号码 25 | QQ = "1234" 26 | 27 | # QQ 密码 28 | QQ_PWD = "" 29 | QQ_GROUP_NICK=u'BSD-小坏蛋 ' 30 | 31 | # 日志路径 32 | LOG_PATH = "log.log" 33 | 34 | # 日志最大大小 35 | LOG_MAX_SIZE = 5 * 1024 * 1024 # 5M 36 | 37 | # 日志最大备份数目 38 | LOG_BACKUP_COUNT = 10 39 | 40 | # 自动同意好友申请 41 | # 需要将身份验证设置为"需要验证信息" 42 | # 好友备注名将自动改为好友的QQ号 43 | AUTO_ACCEPT = True 44 | 45 | # 下面是有道辞典需要的api, 可以到下面网站申请一个key和keyfrom 46 | # http://fanyi.youdao.com/openapi 47 | YOUDAO_KEY = 877169102 48 | 49 | YOUDAO_KEYFROM = "testqq" 50 | 51 | 52 | # 允许机器人发送消息的最大长度 53 | # 此配置避免结果过长在群内造成刷屏 54 | MAX_LENGTH = 150 55 | 56 | # 机器人接收内容超过这个长度将贴到网上 57 | MAX_RECEIVER_LENGTH = 300 58 | 59 | # 是否上传验证图片, False则存在本地 60 | UPLOAD_CHECKIMG = False 61 | 62 | # 是否启动SimSimi应答 63 | SimSimi_Enabled = True 64 | 65 | # SimSimi代理防止ip被官方封掉 66 | SimSimi_Proxy = ("host", "port") 67 | 68 | # 需要改动QQ某些东西, 比如设置签名, 所要提供的密码 69 | Set_Password = "set_password" 70 | 71 | # 两条消息最小时间间隔, bot 连续发送消息, 如果频率过快会被tx过滤掉 72 | # 设置一个时间间隔来确保消息被正常投递, 安全值是0.5, 其他更小的值未测试 73 | MESSAGE_INTERVAL = 0.5 74 | 75 | # 是否启用一个HTTP服务器来输入验证码 76 | # 启用这个将按照下面的配置启用一个HTTP Server提供输入验证码的接口 77 | HTTP_CHECKIMG = False 78 | 79 | # HTTP 验证码服务器监听地址 80 | HTTP_LISTEN = "0.0.0.0" 81 | 82 | # HTTP 验证码服务器监听端口 83 | HTTP_PORT = 8000 84 | 85 | 86 | # 是否启用提醒(当程序重启需要验证码时发送邮件通知) 87 | # 邮箱可以用类似126的手机号邮箱会及时发送短信提醒 88 | EMAIL_NOTICE = False 89 | 90 | # 发送邮件使用SMTP方式, 91 | # 指定SMTP地址 92 | SMTP_HOST = "smtp.126.com" 93 | 94 | # 指定发件账号 95 | SMTP_ACCOUNT = "account" 96 | 97 | # 指定发件密码 98 | SMTP_PASSWORD = "aa" 99 | 100 | # 指定接收提醒的邮箱 101 | # 为了及时收到提醒, 请使用可以连通手机的邮箱 102 | EMAIL = "11111111@126.com" 103 | PLUGINS_DIR="../" 104 | SmartRobot_Enabled=True 105 | -------------------------------------------------------------------------------- /plugins/config.py.bak1: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # 3 | # Copyright 2013 cold 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # 是否输出调试信息 19 | DEBUG = True 20 | 21 | # 是否显示详细HTTP跟踪信息 22 | TRACE = False 23 | 24 | # QQ 号码 25 | QQ = 1234 26 | 27 | # QQ 密码 28 | QQ_PWD = "" 29 | QQ_GROUP_NICK=u'BSD-小坏蛋 ' 30 | 31 | # 日志路径 32 | LOG_PATH = "log.log" 33 | 34 | # 日志最大大小 35 | LOG_MAX_SIZE = 5 * 1024 * 1024 # 5M 36 | 37 | # 日志最大备份数目 38 | LOG_BACKUP_COUNT = 10 39 | 40 | # 自动同意好友申请 41 | # 需要将身份验证设置为"需要验证信息" 42 | # 好友备注名将自动改为好友的QQ号 43 | AUTO_ACCEPT = True 44 | 45 | # 下面是有道辞典需要的api, 可以到下面网站申请一个key和keyfrom 46 | # http://fanyi.youdao.com/openapi 47 | YOUDAO_KEY = test 48 | 49 | YOUDAO_KEYFROM = "testqq" 50 | 51 | 52 | # 允许机器人发送消息的最大长度 53 | # 此配置避免结果过长在群内造成刷屏 54 | MAX_LENGTH = 150 55 | 56 | # 机器人接收内容超过这个长度将贴到网上 57 | MAX_RECEIVER_LENGTH = 300 58 | 59 | # 是否上传验证图片, False则存在本地 60 | UPLOAD_CHECKIMG = False 61 | 62 | # 是否启动SimSimi应答 63 | SimSimi_Enabled = True 64 | 65 | # SimSimi代理防止ip被官方封掉 66 | SimSimi_Proxy = ("host", "port") 67 | 68 | # 需要改动QQ某些东西, 比如设置签名, 所要提供的密码 69 | Set_Password = "set_password" 70 | 71 | # 两条消息最小时间间隔, bot 连续发送消息, 如果频率过快会被tx过滤掉 72 | # 设置一个时间间隔来确保消息被正常投递, 安全值是0.5, 其他更小的值未测试 73 | MESSAGE_INTERVAL = 0.5 74 | 75 | # 是否启用一个HTTP服务器来输入验证码 76 | # 启用这个将按照下面的配置启用一个HTTP Server提供输入验证码的接口 77 | HTTP_CHECKIMG = False 78 | 79 | # HTTP 验证码服务器监听地址 80 | HTTP_LISTEN = "0.0.0.0" 81 | 82 | # HTTP 验证码服务器监听端口 83 | HTTP_PORT = 8000 84 | 85 | 86 | # 是否启用提醒(当程序重启需要验证码时发送邮件通知) 87 | # 邮箱可以用类似126的手机号邮箱会及时发送短信提醒 88 | EMAIL_NOTICE = False 89 | 90 | # 发送邮件使用SMTP方式, 91 | # 指定SMTP地址 92 | SMTP_HOST = "smtp.126.com" 93 | 94 | # 指定发件账号 95 | SMTP_ACCOUNT = "account" 96 | 97 | # 指定发件密码 98 | SMTP_PASSWORD = "aa" 99 | 100 | # 指定接收提醒的邮箱 101 | # 为了及时收到提醒, 请使用可以连通手机的邮箱 102 | EMAIL = "11111111@126.com" 103 | PLUGINS_DIR="../" 104 | SmartRobot_Enabled=True 105 | -------------------------------------------------------------------------------- /plugins/shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : rootntsd 5 | # E-mail : rootntsd@gmail.com 6 | # Date : 14/01/26 0:35:00 7 | # Desc : ''' 8 | # 9 | #from __init__ import BasePlugin 10 | from plugins import BasePlugin 11 | import logging 12 | import inspect 13 | import os 14 | 15 | logger = logging.getLogger("plugin") 16 | 17 | def callit(obj, mname, args=[]): 18 | mth = eval('obj.%s'%(mname)) 19 | if callable(mth): 20 | return apply(mth, args) 21 | else: 22 | return mth 23 | 24 | class ShellPlugin(BasePlugin): 25 | pre_fix="!@#" 26 | def is_match(self, from_uin, content, type): 27 | self.send_content='' 28 | if content.startswith(self.pre_fix): 29 | try: 30 | content=content.strip(self.pre_fix); 31 | # if content.startswith('list'): 32 | # content=content.lstrip('list') 33 | # if content=='': 34 | # self.send_content=dir(self) 35 | # else: 36 | # self.send_content=inspect.getargspec(getattr(self,content.lstrip(' '))) 37 | # pass 38 | # elif content=='show': 39 | # self.send_content='show' 40 | # pass 41 | # else: 42 | # if hasattr(self,content): 43 | funname=content.split(" ")[0] 44 | content=content.strip(funname) 45 | #fun=getattr(self,funname) 46 | self.send_content=self.send_content+' '+funname 47 | print 'content:',content 48 | args=[] 49 | if content!='': 50 | args=list(content.split(',')) 51 | # if args[0]=='': 52 | # args=[] 53 | print args,len(args) 54 | self.send_content=callit(self,funname,args) 55 | # else: 56 | # #self.send_content=dir('self.'+content) 57 | # pass 58 | pass 59 | except Exception,e: 60 | self.send_content=e 61 | logger.error(u"Plugin was encoutered an error {0}".format(e), exc_info = True) 62 | return True 63 | return False 64 | 65 | def handle_message(self, callback): 66 | callback(self.send_content) 67 | return True 68 | def test(self): 69 | print 'test' 70 | return 'hello' 71 | def ls(self,args=[]): 72 | return os.popen('ls '+''.join(args)).read() 73 | 74 | if __name__=="__main__": 75 | s=ShellPlugin(None,None,None,None) 76 | s.is_match(1,'!@#test','s') 77 | s.is_match(1,'!@#pre_fix','s') 78 | print s.send_content 79 | -------------------------------------------------------------------------------- /plugins/translate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 14/01/16 12:23:34 7 | # Desc : 翻译插件 8 | # 9 | import json 10 | import traceback 11 | 12 | import config 13 | 14 | from plugins import BasePlugin 15 | 16 | class TranslatePlugin(BasePlugin): 17 | def is_match(self, from_uin, content, type): 18 | if content.startswith("-tr"): 19 | web = content.startswith("-trw") 20 | self.is_web = web 21 | self.body = content.lstrip("-trw" if web else "-tr").strip() 22 | return True 23 | return False 24 | 25 | def handle_message(self, callback): 26 | key = config.YOUDAO_KEY 27 | keyfrom = config.YOUDAO_KEYFROM 28 | source = self.body.encode("utf-8") 29 | url = "http://fanyi.youdao.com/openapi.do" 30 | params = [("keyfrom", keyfrom), ("key", key),("type", "data"), 31 | ("doctype", "json"), ("version",1.1), ("q", source)] 32 | self.http.get(url, params, callback = self.read_result, 33 | kwargs = {"callback":callback}) 34 | 35 | def read_result(self, resp, callback): 36 | web = self.is_web 37 | try: 38 | result = json.loads(resp.body) 39 | except ValueError: 40 | self.logger.warn(traceback.format_exc()) 41 | body = u"error" 42 | else: 43 | errorCode = result.get("errorCode") 44 | if errorCode == 0: 45 | query = result.get("query") 46 | r = " ".join(result.get("translation")) 47 | basic = result.get("basic", {}) 48 | body = u"{0}\n{1}".format(query, r) 49 | phonetic = basic.get("phonetic") 50 | if phonetic: 51 | ps = phonetic.split(",") 52 | if len(ps) == 2: 53 | pstr = u"读音: 英 [{0}] 美 [{1}]".format(*ps) 54 | else: 55 | pstr = u"读音: {0}".format(*ps) 56 | body += u"\n" + pstr 57 | 58 | exp = basic.get("explains") 59 | if exp: 60 | body += u"\n其他释义:\n\t{0}".format(u"\n\t".join(exp)) 61 | 62 | if web: 63 | body += u"\n网络释义:\n" 64 | web = result.get("web", []) 65 | if web: 66 | for w in web: 67 | body += u"\t{0}\n".format(w.get("key")) 68 | vs = u"\n\t\t".join(w.get("value")) 69 | body += u"\t\t{0}\n".format(vs) 70 | 71 | if errorCode == 50: 72 | body = u"无效的有道key" 73 | 74 | if not body: 75 | body = u"没有结果" 76 | 77 | callback(body) 78 | -------------------------------------------------------------------------------- /plugins/command.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 14/01/16 12:18:53 7 | # Desc : 相应基本命令插件 8 | # 9 | import time 10 | 11 | from datetime import datetime 12 | 13 | from plugins import BasePlugin 14 | from plugins import config 15 | 16 | 17 | class CommandPlugin(BasePlugin): 18 | def uptime(self): 19 | up_time = datetime.fromtimestamp(self.webqq.start_time)\ 20 | .strftime("%H:%M:%S") 21 | now = time.time() 22 | 23 | sub = int(now - self.webqq.start_time) 24 | num, unit, oth = None, None, "" 25 | if sub < 60: 26 | num, unit = sub, "sec" 27 | elif sub > 60 and sub < 3600: 28 | num, unit = sub / 60, "min" 29 | elif sub > 3600 and sub < 86400: 30 | num = sub / 3600 31 | unit = "" 32 | num = "{0}:{1}".format("%02d" % num, 33 | ((sub - (num * 3600)) / 60)) 34 | elif sub > 86400: 35 | num, unit = sub / 84600, "days" 36 | h = (sub - (num * 86400)) / 3600 37 | m = (sub - ((num * 86400) + h * 3600)) / 60 38 | if h or m: 39 | oth = ", {0}:{1}".format(h, m) 40 | 41 | return "{0} up {1} {2} {3}, handled {4} message(s)"\ 42 | .format(up_time, num, unit, oth, self.webqq.msg_num) 43 | 44 | def msgon(self): 45 | config.SmartRobot_Enabled=True 46 | return u"消息群开启!!" 47 | def msgoff(self): 48 | config.SmartRobot_Enabled=False 49 | return u"消息群关闭!!!" 50 | def is_match(self, from_uin, content, type): 51 | ABOUT_STR = u"\nAuthor : evilbinary小E\nE-mail : rootntsd@gmail.com\n"\ 52 | u"HomePage : http://evilbinary.org\n"\ 53 | u"Project@ : https://github.com/evilbinary" 54 | HELP_DOC = u"\n====命令列表====\n"\ 55 | u"help 显示此信息\n"\ 56 | u"ping 确定机器人是否在线\n"\ 57 | u"about 查看关于该机器人项目的信息\n"\ 58 | u"执行代码 lang:[语言] code:[代码]\n"\ 59 | u">>> [代码] 执行Python语句\n"\ 60 | u"-w [城市] 查询城市今明两天天气\n"\ 61 | u"-tr [单词] 中英文互译\n"\ 62 | u"-pm25 [城市] 查询城市当天PM2.5情况等\n"\ 63 | u"-sleep 关群消息\n"\ 64 | u"====命令列表====" 65 | ping_cmd = "ping" 66 | about_cmd = "about" 67 | help_cmd = "help" 68 | msgoff_cmd="-sleep" 69 | msgon_cmd="-wake" 70 | commands = [ping_cmd, about_cmd, help_cmd, "uptime",msgoff_cmd,msgon_cmd] 71 | command_resp = {ping_cmd:u"小的在", about_cmd:ABOUT_STR, 72 | help_cmd:HELP_DOC, 73 | "uptime":self.uptime,msgoff_cmd:self.msgoff,msgon_cmd:self.msgon} 74 | 75 | if content.encode("utf-8").strip().lower() in commands: 76 | body = command_resp[content.encode("utf-8").strip().lower()] 77 | if not isinstance(body, (str, unicode)): 78 | body = body() 79 | self.body = body 80 | print "True::::" 81 | return True 82 | 83 | def handle_message(self, callback): 84 | callback(self.body) 85 | -------------------------------------------------------------------------------- /plugins/douban.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 14/01/20 10:33:47 7 | # Desc : 爬取豆瓣书籍/电影/歌曲信息 8 | # 9 | from bs4 import BeautifulSoup 10 | 11 | try: 12 | from plugins import BasePlugin 13 | except: 14 | BasePlugin = object 15 | 16 | class DoubanReader(object): 17 | def __init__(self, http): 18 | self.http = http 19 | self.url = "http://www.douban.com/search" 20 | 21 | def search(self, name, callback): 22 | params = {"q":name.encode("utf-8")} 23 | self.http.get(self.url, params, callback = self.parse_html, 24 | kwargs = {"callback":callback}) 25 | 26 | def parse_html(self, response, callback): 27 | soup = BeautifulSoup(response.body) 28 | item = soup.find(attrs = {"class":"content"}) 29 | if item: 30 | try: 31 | type = item.find("span").text 32 | a = item.find('a') 33 | name = a.text 34 | href = a.attrs["href"] 35 | rating = item.find(attrs = {"class":"rating_nums"}).text 36 | cast = item.find(attrs={"class":"subject-cast"}).text 37 | desc = item.find("p").text 38 | except AttributeError: 39 | callback(u"没有找到相关信息") 40 | return 41 | 42 | if type == u"[电影]": 43 | cast_des = u"原名/导演/主演/年份" if len(cast.split("/")) == 4\ 44 | else u"导演/主演/年份" 45 | elif type == u"[书籍]": 46 | cast_des = u"作者/译者/出版社/年份" if len(cast.split("/")) == 4\ 47 | else u"作者/出版社/年份" 48 | body = u"{0}{1}:\n"\ 49 | u"评分: {2}\n"\ 50 | u"{3}: {4}\n"\ 51 | u"描述: {5}\n"\ 52 | u"详细信息: {6}\n"\ 53 | .format(type, name, rating, cast_des, cast, desc, href) 54 | else: 55 | body = u"没有找到相关信息" 56 | 57 | callback(body) 58 | 59 | 60 | class DoubanPlugin(BasePlugin): 61 | douban = None 62 | def is_match(self, from_uin, content, type): 63 | if (content.startswith("<") and content.endswith(">")) or\ 64 | (content.startswith(u"《") and content.endswith(u"》")): 65 | self._name = content.strip("<").strip(">").strip(u"《")\ 66 | .strip(u"》") 67 | 68 | if not self._name.strip(): 69 | return False 70 | 71 | if self.douban is None: 72 | self.douban = DoubanReader(self.http) 73 | return True 74 | return False 75 | 76 | 77 | def handle_message(self, callback): 78 | self.douban.search(self._name, callback) 79 | 80 | if __name__ == "__main__": 81 | from tornadohttpclient import TornadoHTTPClient 82 | def cb(b): 83 | print b 84 | douban = DoubanReader(TornadoHTTPClient()) 85 | douban.search(u"百年孤独", cb) 86 | douban.search(u"鸟哥的私房菜", cb) 87 | douban.search(u"论语", cb) 88 | douban.search(u"寒战", cb) 89 | douban.search(u"阿凡达", cb) 90 | douban.search(u"创战记", cb) 91 | douban.search(u"简单爱", cb) 92 | TornadoHTTPClient().start() 93 | -------------------------------------------------------------------------------- /plugins/lang.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : evilbinary 5 | # E-mail : rootntsd@gmail.com 6 | # Date : 2014-2-13 23:28 7 | # Desc : ���ýӿ�ʵ������lang���� 8 | # 9 | import re 10 | import logging 11 | from bs4 import BeautifulSoup 12 | from plugins import BasePlugin 13 | 14 | 15 | logger = logging.getLogger("plugin") 16 | 17 | class LangPlugin(BasePlugin): 18 | url = "http://www.compileonline.com/execute_new.php" 19 | result_p = re.compile(r'
(.*?)
', flags=re.U | re.M | re.S) 20 | 21 | def is_match(self, from_uin, content, type): 22 | lang_pos = content.find('lang:') 23 | if lang_pos < 6 and lang_pos >= 0: 24 | 25 | code_pos = content.find('code:') 26 | inputs_pos = content.find('inputs:') 27 | args_pos = content.find('args:') 28 | stdinput_pos = content.find('stdinput:') 29 | if inputs_pos == -1: 30 | inputs_pos = len(content) 31 | 32 | self._lang = content[lang_pos:code_pos][len('lang:'):].strip() 33 | self._code = content[code_pos:inputs_pos][len('code:'):].strip() 34 | self._inputs = content[inputs_pos:args_pos][len('inputs:'):].strip() 35 | self._args = content[args_pos:stdinput_pos][len('args:'):].strip() 36 | self._stdinput = content[stdinput_pos:-1][len('stdinput:'):].strip() 37 | self._header = '' 38 | 39 | print 'lang_pos:', lang_pos, 'code_pos:', code_pos, 'inputs_pos:', inputs_pos, 'args_pos:', args_pos, 'stdinput_pos:', stdinput_pos 40 | print 'lang:', self._lang 41 | print 'code:', self._code 42 | print 'input:', self._inputs 43 | print 'args:', self._args 44 | print 'stdinput:', self._stdinput 45 | 46 | return True 47 | return False 48 | 49 | def handle_message(self, callback): 50 | self.url = "http://www.compileonline.com/execute_new.php" 51 | params = {"args":self._args, "code":self._code.encode("utf-8"), 52 | "inputs":self._stdinput, "lang": self._lang, "stdinput":self._stdinput} 53 | if self._lang in ['c', 'c++', 'c++11', 'c++0x', 'csharp', 'asm', 'ada', 'befunge', 'c99', 'cobol', 'cpp', \ 54 | 'd', 'sdcc', 'erlang', 'fortran', 'fsharp', 'haskell', 'icon', 'ilasm', 'intercal', \ 55 | 'java', 'mozart', 'nimrod', 'objc', 'ocaml', 'pascal', '"pawn', 'qbasic', \ 56 | 'rust', 'scala', 'simula', 'vb.net', 'verilog']: 57 | params['header'] = '' 58 | params['support'] = '' 59 | params['util'] = '' 60 | print params 61 | self.url = "http://www.compileonline.com/compile_new.php" 62 | 63 | 64 | def read(resp): 65 | body = resp.body 66 | if len(resp.body) > 400: 67 | body = resp.body[0:400] 68 | try: 69 | logger.info(u"Lisp request success, result: {0}".format(body)) 70 | except Exception, e: 71 | logger.info(u'except:{0}', e) 72 | 73 | try: 74 | soup = BeautifulSoup(resp.body) 75 | pre = soup.find_all('pre') 76 | result = '' 77 | if len(pre) == 0: 78 | result = soup.get_text() 79 | for p in pre: 80 | result = result + p.get_text() 81 | except Exception, e: 82 | logger.info(u'except:{0}', e) 83 | result = resp.body 84 | # result = self.result_p.findall(resp.body) 85 | # if not result else result[0] 86 | 87 | callback(result) 88 | 89 | self.http.post(self.url, params, callback=read) 90 | 91 | -------------------------------------------------------------------------------- /twqq/_hash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 13/11/07 13:49:42 7 | # Desc : 8 | # 9 | 10 | """ 这个函数翻译自Javascript 11 | 12 | 源在这里: http://pidginlwqq.sinaapp.com/hash.js 13 | 14 | 如果无法正常获取好友列表, 请查看上面js是否有更新, 有则更新本函数 15 | Update On: 2014-06-15 16 | """ 17 | # def webqq_hash(b, i): 18 | # if isinstance(b, basestring): 19 | # b = int(b) 20 | # a = [0, 0, 0, 0] 21 | # for s in range(len(i)): 22 | # a[s % 4] ^= ord(i[s]) 23 | 24 | # j = ["EC", "OK"] 25 | # d = range(4) 26 | # d[0] = b >> 24 & 255 ^ ord(j[0][0]) 27 | # d[1] = b >> 16 & 255 ^ ord(j[0][1]) 28 | # d[2] = b >> 8 & 255 ^ ord(j[1][0]) 29 | # d[3] = b & 255 ^ ord(j[1][1]) 30 | 31 | # j = range(8) 32 | # for s in range(8): 33 | # j[s] = a[s>>1] if s % 2 == 0 else d[s>>1] 34 | 35 | # a = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", 36 | # "B", "C", "D", "E", "F"] 37 | # d = "" 38 | # for s in range(len(j)): 39 | # d += a[j[s] >> 4 & 15] 40 | # d += a[j[s] & 15] 41 | 42 | # return d 43 | 44 | 45 | # def webqq_hash(i, a): 46 | # if isinstance(i, (str, unicode)): 47 | # i = int(i) 48 | # class b: 49 | # def __init__(self, _b, i): 50 | # self.s = _b or 0 51 | # self.e = i or 0 52 | 53 | # r = [i >> 24 & 255, i >> 16 & 255, i >> 8 & 255, i & 255] 54 | 55 | # j = [ord(_a) for _a in a] 56 | 57 | # e = [b(0, len(j) - 1)] 58 | # while len(e) > 0: 59 | # c = e.pop() 60 | # if not (c.s >= c.e or c.s < 0 or c.e > len(j)): 61 | # if c.s+1 == c.e: 62 | # if (j[c.s] > j[c.e]) : 63 | # l = j[c.s] 64 | # j[c.s] = j[c.e] 65 | # j[c.e] = l 66 | # else: 67 | # l = c.s 68 | # J = c.e 69 | # f=j[c.s] 70 | # while c.s < c.e: 71 | # while c.s < c.e and j[c.e]>=f: 72 | # c.e -= 1 73 | # r[0] = r[0] + 3&255 74 | 75 | # if c.s < c.e: 76 | # j[c.s] = j[c.e] 77 | # c.s += 1 78 | # r[1] = r[1] * 13 + 43 & 255 79 | 80 | # while c.s < c.e and j[c.s] <= f: 81 | # c.s += 1 82 | # r[2] = r[2] - 3 & 255 83 | 84 | # if c.s < c.e: 85 | # j[c.e] = j[c.s] 86 | # c.e -= 1 87 | # r[3] = (r[0] ^ r[1]^r[2]^r[3]+1) & 255 88 | # j[c.s] = f 89 | # e.append(b(l, c.s-1)) 90 | # e.append(b(c.s + 1, J)) 91 | # j = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", 92 | # "B", "C", "D", "E", "F"] 93 | # e = "" 94 | # for c in range(len(r)): 95 | # e += j[r[c]>>4&15] 96 | # e += j[r[c]&15] 97 | 98 | # return e 99 | 100 | def webqq_hash(b, j): 101 | b = str(b) 102 | a = j + "password error" 103 | i = "" 104 | 105 | while True: 106 | if len(i) <= len(a): 107 | i += b 108 | if len(i) == len(a): 109 | break 110 | else: 111 | i = i[:len(a)]; 112 | break 113 | 114 | E = [0] * len(i) 115 | c = 0 116 | while c < len(i): 117 | E[c] = ord(i[c]) ^ ord(a[c]) 118 | c += 1 119 | a = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"] 120 | i = "" 121 | c = 0 122 | while c < len(E): 123 | i += a[E[c] >> 4 & 15] 124 | i += a[E[c] & 15] 125 | c += 1 126 | return i 127 | -------------------------------------------------------------------------------- /robot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 14/01/16 11:33:32 7 | # Desc : SimSimi插件 8 | # 9 | import json 10 | 11 | from tornadohttpclient import TornadoHTTPClient 12 | 13 | import config 14 | from smartrobot import Searcher 15 | 16 | from plugins import BasePlugin 17 | 18 | 19 | class SimSimiTalk(object): 20 | """ 模拟浏览器与SimSimi交流 21 | 22 | :params http: HTTP 客户端实例 23 | :type http: ~tornadhttpclient.TornadoHTTPClient instance 24 | """ 25 | def __init__(self, http = None): 26 | self.http = http or TornadoHTTPClient() 27 | 28 | if not http: 29 | self.http.set_user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 Chrome/28.0.1500.71 Safari/537.36") 30 | self.http.debug = getattr(config, "TRACE", False) 31 | self.http.validate_cert = False 32 | self.http.set_global_headers({"Accept-Charset": "UTF-8,*;q=0.5"}) 33 | 34 | self.url = "http://www.simsimi.com/func/req" 35 | self.params = {"lc":"zh", "ft":0.0} 36 | self.ready = False 37 | 38 | self.fetch_kwargs = {} 39 | if config.SimSimi_Proxy: 40 | self.fetch_kwargs.update(proxy_host = config.SimSimi_Proxy[0], 41 | proxy_port = config.SimSimi_Proxy[1]) 42 | 43 | self._setup_cookie() 44 | 45 | 46 | def _setup_cookie(self): 47 | def callback(resp): 48 | self.ready = True 49 | 50 | self.http.get("http://www.simsimi.com", callback = callback) 51 | 52 | 53 | def talk(self, msg, callback): 54 | """ 聊天 55 | 56 | :param msg: 信息 57 | :param callback: 接收响应的回调 58 | """ 59 | headers = {"Referer":"http://www.simsimi.com/talk.htm", 60 | "Accept":"application/json, text/javascript, */*; q=0.01", 61 | "Accept-Language":"zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3", 62 | "Content-Type":"application/json; charset=utf-8", 63 | "X-Requested-With":"XMLHttpRequest", 64 | } 65 | if not msg.strip(): 66 | return callback(u"小的在") 67 | params = {"msg":msg.encode("utf-8")} 68 | params.update(self.params) 69 | 70 | def _talk(resp): 71 | data = {} 72 | if resp.body: 73 | try: 74 | data = json.loads(resp.body) 75 | except ValueError: 76 | pass 77 | callback(data.get("response", "Server respond nothing!")) 78 | 79 | self.http.get(self.url, params, headers = headers, 80 | callback = _talk) 81 | 82 | 83 | class SimSimi2Plugin(BasePlugin): 84 | simsimi = None 85 | searcher=None 86 | 87 | def is_match(self, form_uin, content, type): 88 | if not getattr(config, "SimSimi_Enabled", False): 89 | return False 90 | else: 91 | self.searcher=Searcher() 92 | 93 | print 'search' 94 | if self.searcher.find(content): 95 | result=self.searcher.search(content) 96 | if result>=0 : 97 | return True 98 | return True 99 | 100 | def handle_message(self, callback): 101 | callback('abc') 102 | # self.simsimi.talk(self.content, callback) 103 | 104 | 105 | if __name__ == "__main__": 106 | import threading,time 107 | simsimi = SimSimiTalk() 108 | def callback(response): 109 | print response 110 | simsimi.http.stop() 111 | 112 | def talk(): 113 | while 1: 114 | if simsimi.ready: 115 | simsimi.talk("nice to meet you", callback) 116 | break 117 | else: 118 | time.sleep(1) 119 | 120 | t = threading.Thread(target = talk) 121 | t.setDaemon(True) 122 | t.start() 123 | simsimi.http.start() 124 | -------------------------------------------------------------------------------- /plugins/pm25.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 14/01/16 16:15:59 7 | # Desc : PM2.5 查询插件 8 | # 9 | """ 代码贡献自 EricTang (汤勺), 由 cold 整理 10 | """ 11 | from _pinyin import PinYin 12 | from bs4 import BeautifulSoup 13 | 14 | from plugins import BasePlugin 15 | 16 | 17 | PM25_URL = 'http://www.pm25.in/' 18 | 19 | class PM25Plugin(BasePlugin): 20 | def is_match(self, from_uin, content, type): 21 | if content.startswith("-pm25"): 22 | self.city = content.split(" ")[1] 23 | self._format = u"\n {0}" if type == "g" else u"{0}" 24 | return True 25 | return False 26 | 27 | def handle_message(self, callback): 28 | self.getPM25_by_city(self.convert2pinyin(self.city), callback) 29 | 30 | def getPM25_by_city(self, city, callback): 31 | """ 32 | 根据城市查询PM25值 33 | """ 34 | self._city = city.encode("utf-8") 35 | if city: 36 | url = PM25_URL + city.encode("utf-8") 37 | self.http.get(url, callback = self.callback, 38 | kwargs = {"callback":callback}) 39 | else: 40 | callback(u'没输入城市你让我查个头啊...') 41 | 42 | def callback(self, resp, callback): 43 | html_doc = resp.body 44 | soup = BeautifulSoup(html_doc) 45 | #美丽的汤获取的数组,找到城市的PM25 46 | city_air_data_array = soup.find_all(attrs = {'class':'span12 data'}) 47 | 48 | #获取数据更新时间 49 | city_aqi_update_array = \ 50 | str(soup.find_all(attrs = {'class':'live_data_time'})[0])\ 51 | .replace('
\n','') 52 | 53 | city_aqi_update_time = city_aqi_update_array.replace('
', '').strip() 54 | city_aqi_update_time = city_aqi_update_time.replace('

', '') 55 | city_aqi_update_time = city_aqi_update_time.replace('

', '') 56 | 57 | 58 | #获取城市名 59 | target_city = "h2" 60 | city_name_str = str(soup.find_all(target_city)[0])\ 61 | .replace('<%s>' % target_city,'') 62 | city_name = city_name_str.replace('' % target_city,'').strip() 63 | 64 | #获取城市空气质量 65 | target_city_aqi = "h4" 66 | city_aqi_str = str(soup.find_all(target_city_aqi)[0])\ 67 | .replace('<%s>' % target_city_aqi,'') 68 | city_aqi = city_aqi_str.replace('' % target_city_aqi,'').strip() 69 | 70 | 71 | #获取城市各项指标的数据值,切割 72 | city_data_array = str(city_air_data_array[0]).strip()\ 73 | .split('
\n') 74 | city_data_array.remove('
\n') 75 | city_data_array = [x.replace('
\n','').strip() 76 | for x in city_data_array] 77 | city_data_array = [x.replace('
\n','').strip() 78 | for x in city_data_array] 79 | city_data_array = [x.replace('
\n
','').strip() 80 | for x in city_data_array] 81 | city_data_array = [x.replace('
\n','').strip() 82 | for x in city_data_array] 83 | city_data_array = [x.replace('\n','').strip() 84 | for x in city_data_array] 85 | city_data_array = [x.lstrip().rstrip() 86 | for x in city_data_array] 87 | city_data_array.pop() 88 | 89 | city_air_status_str=u"当前查询城市为:{0},空气质量为:{1}\n{2}\n"\ 90 | u"{3}\n点击链接查看完整空气质量报告:{4}{5}"\ 91 | .format (city_name.decode("utf-8"), city_aqi.decode("utf-8"), 92 | "\n".join(city_data_array).decode("utf-8"), 93 | city_aqi_update_time.decode("utf-8"), PM25_URL, 94 | self._city) 95 | 96 | callback(city_air_status_str) 97 | 98 | def convert2pinyin(self, words): 99 | """ 100 | 将中文转换为拼音 101 | """ 102 | if words: 103 | pinyin = PinYin() 104 | pinyin.load_word() 105 | pinyin_array=pinyin.hanzi2pinyin(string=words) 106 | return "".join(pinyin_array) 107 | else: 108 | return '' 109 | -------------------------------------------------------------------------------- /plugins/simsimi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 14/01/16 11:33:32 7 | # Desc : SimSimi插件 8 | # 9 | import json 10 | 11 | from tornadohttpclient import TornadoHTTPClient 12 | 13 | import config 14 | import random 15 | 16 | from plugins import BasePlugin 17 | 18 | 19 | class SimSimiTalk(object): 20 | """ 模拟浏览器与SimSimi交流 21 | 22 | :params http: HTTP 客户端实例 23 | :type http: ~tornadhttpclient.TornadoHTTPClient instance 24 | """ 25 | def __init__(self, http = None): 26 | self.http = http or TornadoHTTPClient() 27 | 28 | if not http: 29 | self.http.set_user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 Chrome/28.0.1500.71 Safari/537.36") 30 | self.http.debug = getattr(config, "TRACE", False) 31 | self.http.validate_cert = False 32 | self.http.set_global_headers({"Accept-Charset": "UTF-8,*;q=0.5"}) 33 | 34 | self.url = "http://www.simsimi.com/func/req" 35 | self.params = {"lc":"zh", "ft":0.0} 36 | self.ready = False 37 | 38 | self.fetch_kwargs = {} 39 | if config.SimSimi_Proxy: 40 | self.fetch_kwargs.update(proxy_host = config.SimSimi_Proxy[0], 41 | proxy_port = config.SimSimi_Proxy[1]) 42 | 43 | self._setup_cookie() 44 | 45 | 46 | def _setup_cookie(self): 47 | def callback(resp): 48 | self.ready = True 49 | 50 | self.http.get("http://www.simsimi.com", callback = callback) 51 | 52 | 53 | def talk(self, msg, callback): 54 | """ 聊天 55 | 56 | :param msg: 信息 57 | :param callback: 接收响应的回调 58 | """ 59 | headers = {"Referer":"http://www.simsimi.com/talk.htm", 60 | "Accept":"application/json, text/javascript, */*; q=0.01", 61 | "Accept-Language":"zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3", 62 | "Content-Type":"application/json; charset=utf-8", 63 | "X-Requested-With":"XMLHttpRequest", 64 | } 65 | if not msg.strip(): 66 | return callback(u"小的在") 67 | params = {"msg":msg.encode("utf-8")} 68 | params.update(self.params) 69 | 70 | def _talk(resp): 71 | data = {} 72 | if resp.body: 73 | try: 74 | data = json.loads(resp.body) 75 | except ValueError: 76 | pass 77 | callback(data.get("。。。。")) 78 | 79 | self.http.get(self.url, params, headers = headers, 80 | callback = _talk) 81 | 82 | 83 | class SimSimiPlugin(BasePlugin): 84 | simsimi = None 85 | 86 | def is_match(self, form_uin, content, type): 87 | if not getattr(config, "SimSimi_Enabled", False): 88 | return False 89 | else: 90 | self.simsimi = SimSimiTalk() 91 | 92 | if type == "g" and random.choice('abcd')!='a': 93 | if self.nickname !=None: 94 | if content.startswith(self.nickname.strip()) or \ 95 | content.endswith(self.nickname.strip()) or \ 96 | content.startswith(config.QQ_GROUP_NICK.strip()) or \ 97 | content.endswith(config.QQ_GROUP_NICK.strip()) or \ 98 | content.startswith('@'+config.QQ_GROUP_NICK.strip()) or \ 99 | content.endswith('@'+config.QQ_GROUP_NICK.strip()): 100 | 101 | self.content = content.strip(self.nickname).strip(config.QQ_GROUP_NICK.strip()) 102 | 103 | return True 104 | else: 105 | return False 106 | else: 107 | self.content = content 108 | return True 109 | return False 110 | 111 | def handle_message(self, callback): 112 | self.simsimi.talk(self.content, callback) 113 | 114 | 115 | if __name__ == "__main__": 116 | import threading,time 117 | simsimi = SimSimiTalk() 118 | def callback(response): 119 | print response 120 | simsimi.http.stop() 121 | 122 | def talk(): 123 | while 1: 124 | if simsimi.ready: 125 | simsimi.talk("nice to meet you", callback) 126 | break 127 | else: 128 | time.sleep(1) 129 | 130 | t = threading.Thread(target = talk) 131 | t.setDaemon(True) 132 | t.start() 133 | simsimi.http.start() 134 | -------------------------------------------------------------------------------- /twqq/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 13/11/12 14:55:02 7 | # Desc : 8 | # 9 | import inspect 10 | import logging 11 | 12 | from abc import abstractmethod 13 | 14 | from hub import RequestHub 15 | from requests import BeforeLoginRequest 16 | from requests import (group_message_handler, buddy_message_handler, 17 | kick_message_handler, sess_message_handler, 18 | system_message_handler, discu_message_handler) 19 | 20 | logger = logging.getLogger("twqq") 21 | 22 | 23 | class WebQQClient(object): 24 | 25 | """ Webqq 模拟客户端 26 | 27 | :param qq: QQ号 28 | :param pwd: 密码 29 | """ 30 | 31 | def __init__(self, qq, pwd, debug=False): 32 | # self.msg_disp = MessageDispatch(self) 33 | self.setup_msg_handlers() 34 | self.setup_request_handlers() 35 | self.hub = RequestHub(qq, pwd, self, debug, handle_msg_image=False) 36 | 37 | @abstractmethod 38 | def handle_verify_code(self, path, r, uin): 39 | """ 重写此函数处理验证码 40 | 41 | :param path: 验证码图片路径 42 | :param r: 接口返回 43 | :param uin: 接口返回 44 | """ 45 | pass 46 | 47 | def enter_verify_code(self, code, r, uin): 48 | """ 填入验证码 49 | 50 | :param code: 验证码 51 | """ 52 | self.hub.check_code = code.strip().lower() 53 | pwd = self.hub.handle_pwd(r, self.hub.check_code.upper(), uin) 54 | self.hub.load_next_request(BeforeLoginRequest(pwd)) 55 | 56 | @group_message_handler 57 | def log_group_message(self, member_nick, content, group_code, 58 | send_uin, source): 59 | """ 对群消息进行日志记录 60 | 61 | :param member_nick: 群昵称 62 | :param content: 消息内容 63 | :param group_code:组代码 64 | :param send_uin: 发送人的uin 65 | :param source: 消息原包 66 | """ 67 | logger.info(u"[群消息] {0}: {1} ==> {2}" 68 | .format(group_code, member_nick, content)) 69 | 70 | @buddy_message_handler 71 | def log_buddy_message(self, from_uin, content, source): 72 | """ 对好友消息进行日志记录 73 | 74 | :param from_uin: 发送人uin 75 | :param content: 内容 76 | :param source: 消息原包 77 | """ 78 | logger.info(u"[好友消息] {0} ==> {1}" 79 | .format(from_uin, content)) 80 | 81 | @sess_message_handler 82 | def log_sess_message(self, qid, from_uin, content, source): 83 | """ 记录临时消息日志 84 | 85 | :param qid: 临时消息的qid 86 | :param from_uin: 发送人uin 87 | :param content: 内容 88 | :param source: 消息原包 89 | """ 90 | logger.info(u"[临时消息] {0} ==> {1}" 91 | .format(from_uin, content)) 92 | 93 | @discu_message_handler 94 | def log_discu_message(self, did, from_uin, content, source): 95 | """ 记录讨论组消息日志 96 | 97 | :param did: 讨论组id 98 | :param from_uin: 消息发送人 uin 99 | :param content: 内容 100 | :param source: 源消息 101 | """ 102 | logger.info(u"[讨论组消息] {0} ==> {1}" 103 | .format(did, content)) 104 | 105 | @kick_message_handler 106 | def log_kick_message(self, message): 107 | """ 被T除的消息 108 | """ 109 | logger.info(u"其他地方登录了此QQ{0}".format(message)) 110 | 111 | @system_message_handler 112 | def log_system_message(self, typ, from_uin, account, source): 113 | """ 记录系统消息日志 114 | """ 115 | logger.info("[系统消息]: 类型:{0}, 发送人:{1}, 发送账号:{2}, 源:{3}" 116 | .format(type, from_uin, account, source)) 117 | 118 | def setup_msg_handlers(self): 119 | """ 获取消息处理器, 获取被 twqq.requests.*_message_handler装饰的成员函数 120 | 121 | """ 122 | msg_handlers = {} 123 | for _, handler in inspect.getmembers(self, callable): 124 | if not hasattr(handler, "_twqq_msg_type"): 125 | continue 126 | 127 | if handler._twqq_msg_type in msg_handlers: 128 | msg_handlers[handler._twqq_msg_type].append(handler) 129 | else: 130 | msg_handlers[handler._twqq_msg_type] = [handler] 131 | 132 | self.msg_handlers = msg_handlers 133 | 134 | def setup_request_handlers(self): 135 | """ 获取请求处理器(被twqq.reqeusts.register_request_handler 装饰的函数) 136 | """ 137 | request_handlers = {} 138 | for _, handler in inspect.getmembers(self, callable): 139 | if not hasattr(handler, "_twqq_request"): 140 | continue 141 | 142 | if handler._twqq_request in request_handlers: 143 | request_handlers[handler._twqq_request].append(handler) 144 | else: 145 | request_handlers[handler._twqq_request] = [handler] 146 | 147 | self.request_handlers = request_handlers 148 | 149 | def connect(self): 150 | self.hub.connect() 151 | 152 | def disconnect(self): 153 | self.hub.disconnect() 154 | 155 | def run(self): 156 | if not self.hub.connecting: 157 | self.connect() 158 | self.hub.http.start() 159 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Copyright 2013 cold 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # 19 | # Author : cold 20 | # E-mail : wh_linux@126.com 21 | # Date : 13/11/04 10:39:51 22 | # Desc : 开启一个Server来处理验证码 23 | # 24 | import os 25 | import time 26 | import logging 27 | from tornado.ioloop import IOLoop 28 | from tornado.web import RequestHandler, Application, asynchronous 29 | try: 30 | from config import HTTP_LISTEN 31 | except ImportError: 32 | HTTP_LISTEN = "127.0.0.1" 33 | 34 | try: 35 | from config import HTTP_PORT 36 | except ImportError: 37 | HTTP_PORT = 8000 38 | 39 | logger = logging.getLogger() 40 | 41 | class BaseHandler(RequestHandler): 42 | webqq = None 43 | r = None 44 | uin = None 45 | is_login = False 46 | 47 | 48 | 49 | class CImgHandler(BaseHandler): 50 | def get(self): 51 | data = "" 52 | if self.webqq.verify_img_path and os.path.exists(self.webqq.verify_img_path): 53 | with open(self.webqq.verify_img_path) as f: 54 | data = f.read() 55 | 56 | self.set_header("Content-Type", "image/jpeg") 57 | self.set_header("Content-Length", len(data)) 58 | self.write(data) 59 | 60 | 61 | class CheckHandler(BaseHandler): 62 | is_exit = False 63 | def get(self): 64 | if self.webqq.verify_img_path: 65 | path = self.webqq.verify_img_path 66 | if not os.path.exists(path): 67 | html = "暂不需要验证码" 68 | elif self.webqq.hub.is_wait(): 69 | html = u"等待验证码" 70 | elif self.webqq.hub.is_lock(): 71 | html = u"已经输入验证码, 等待验证" 72 | else: 73 | html = """ 74 | 75 |
76 | 验证码: 77 | 78 |
79 | """ 80 | else: 81 | html = "暂不需要验证码" 82 | self.write(html) 83 | 84 | @asynchronous 85 | def post(self): 86 | if (self.webqq.verify_img_path and 87 | not os.path.exists(self.webqq.verify_img_path)) or\ 88 | self.webqq.hub.is_lock(): 89 | self.write({"status":False, "message": u"暂不需要验证码"}) 90 | return self.finish() 91 | 92 | code = self.get_argument("vertify") 93 | code = code.strip().lower().encode('utf-8') 94 | self.webqq.enter_verify_code(code, self.r, self.uin, self.on_callback) 95 | 96 | def on_callback(self, status, msg = None): 97 | self.write({"status":status, "message":msg}) 98 | self.finish() 99 | 100 | class CheckImgAPIHandler(BaseHandler): 101 | is_exit = False 102 | def get(self): 103 | if self.webqq.hub.is_wait(): 104 | self.write({"status":False, "wait":True}) 105 | return 106 | 107 | if self.webqq.hub.is_lock(): 108 | return self.write({"status":True, "require":False}) 109 | 110 | if self.webqq.verify_img_path and \ 111 | os.path.exists(self.webqq.verify_img_path): 112 | if self.webqq.hub.require_check_time and \ 113 | time.time() - self.webqq.hub.require_check_time > 900: 114 | self.write({"status":False, "message":u"验证码过期"}) 115 | self.is_exit = True 116 | else: 117 | url = "http://{0}/check".format(self.request.host) 118 | self.write({"status":True, "require":True, "url":url}) 119 | return 120 | self.write({"status":True, "require":False}) 121 | 122 | 123 | def on_finish(self): 124 | if self.is_exit: 125 | exit() 126 | 127 | 128 | class SendMessageHandler(BaseHandler): 129 | @asynchronous 130 | def post(self): 131 | tomark = self.get_argument("markname") 132 | msg = self.get_argument("message") 133 | self.webqq.send_msg_with_markname(tomark, msg, self.on_back) 134 | 135 | def on_back(self, status, msg = None): 136 | self.write({"status":status, "message":msg}) 137 | self.finish() 138 | 139 | 140 | 141 | app = Application([(r'/', CheckHandler), (r'/check', CImgHandler), 142 | (r'/api/check', CheckImgAPIHandler), 143 | (r'/api/send', SendMessageHandler), 144 | (r'/api/input', CheckHandler) 145 | ]) 146 | 147 | def http_server_run(webqq): 148 | print("listening %s:%s"%(HTTP_LISTEN, HTTP_PORT)) 149 | app.listen(HTTP_PORT, address = HTTP_LISTEN) 150 | BaseHandler.webqq = webqq 151 | webqq.run(BaseHandler) 152 | -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 14/01/16 11:21:19 7 | # Desc : 插件机制 8 | # Modify : 动态加载插件。rootntsd@gmail.com 9 | import os 10 | import inspect 11 | import logging 12 | import config 13 | import sys 14 | import thread 15 | import threading 16 | import time 17 | import signal 18 | 19 | logger = logging.getLogger("plugin") 20 | 21 | 22 | class BasePlugin(object): 23 | """ 插件基类, 所有插件继承此基类, 并实现 hanlde_message 实例方法 24 | :param webqq: webqq.WebQQClient 实例 25 | :param http: TornadoHTTPClient 实例 26 | :param nickname: QQ 机器人的昵称 27 | :param logger: 日志 28 | """ 29 | def __init__(self, webqq, http, nickname, logger = None): 30 | self.webqq = webqq 31 | self.http = http 32 | self.logger = logger or logging.getLogger("plugin") 33 | self.nickname = nickname 34 | 35 | def is_match(self, from_uin, content, type): 36 | """ 判断内容是否匹配本插件, 如匹配则调用 handle_message 方法 37 | :param from_uin: 发送消息人的uin 38 | :param content: 消息内容 39 | :param type: 消息类型(g: 群, s: 临时, b: 好友) 40 | :rtype: bool 41 | """ 42 | return False 43 | 44 | def handle_message(self, callback): 45 | """ 每个插件需实现此实例方法 46 | :param callback: 发送消息的函数 47 | """ 48 | raise NotImplemented 49 | 50 | 51 | 52 | class MyThread(threading.Thread): 53 | def __init__(self, loader): 54 | threading.Thread.__init__(self) 55 | self.loader=loader 56 | self.is_run=True 57 | def run(self): 58 | try: 59 | while self.is_run: 60 | try: 61 | self.loader.auto_load_plugins() 62 | time.sleep(6) 63 | except KeyboardInterrupt: 64 | logger.info("Exiting...Thread") 65 | self.is_run=False 66 | 67 | except Exception,e: 68 | print 'Error',e 69 | logger.error(u"Plugin auto load was encoutered an error {0}".format(e), exc_info = True) 70 | 71 | class PluginLoader(object): 72 | plugins = {} 73 | plugins_time={} 74 | def __init__(self, webqq): 75 | self.current_path = os.path.abspath(os.path.dirname(__file__)) 76 | self.webqq = webqq 77 | self.auto_load_plugins() 78 | self.auot_load_thread=MyThread(self) 79 | self.auot_load_thread.start() 80 | # for m in self.list_modules(): 81 | # mobj = self.import_module(m) 82 | # if mobj is not None: 83 | # self.load_class(mobj) 84 | logger.info("Load Plugins: {0!r}".format(self.plugins)) 85 | 86 | def list_modules(self): 87 | items = os.listdir(self.current_path) 88 | modules = [item.split(".")[0] for item in items 89 | if item.endswith(".py")] 90 | return modules 91 | 92 | def import_module(self, m): 93 | try: 94 | sys.path.append(config.PLUGINS_DIR) 95 | filename="plugins." + m 96 | if sys.modules.has_key(filename): 97 | mod = sys.modules[filename] 98 | reload(mod) 99 | return mod 100 | else: 101 | return __import__(filename, fromlist=["plugins"]) 102 | except: 103 | logger.warn("Error was encountered on loading {0}, will ignore it" 104 | .format(m), exc_info = True) 105 | return None 106 | 107 | return None 108 | def load_class(self, m): 109 | for key, val in m.__dict__.items(): 110 | if inspect.isclass(val) and issubclass(val, BasePlugin) and \ 111 | val != BasePlugin: 112 | self.plugins[key] = val(self.webqq, self.webqq.hub.http, 113 | self.webqq.hub.nickname, logger) 114 | logger.info("Load Plugins {0!r}succes!".format(key)) 115 | 116 | def auto_load_plugins(self): 117 | for m in self.list_modules(): 118 | file_name= self.current_path+'/'+m+'.py' 119 | if self.plugins_time.has_key(m): 120 | #print 'key:',m 121 | a=self.plugins_time[m] 122 | b=os.stat(file_name) 123 | #print 'a:', a.st_atime,' b:',b.st_atime 124 | if a==b: 125 | #print 'equ' 126 | pass 127 | else: 128 | print 'x',m,b 129 | logger.info("Load Plugins1: {0!r}".format(m)) 130 | if os.path.isfile(self.current_path+'/'+m+'.pyc'): 131 | #os.remove(self.current_path+'/'+m+'.pyc') 132 | pass 133 | self.plugins_time[m]=b 134 | mobj = self.import_module(m) 135 | #mobj=self.reload_module(m) 136 | if mobj is not None: 137 | self.load_class(mobj) 138 | else: 139 | logger.info("Load Plugins: {0!r}".format(m)) 140 | print "Load Plugins:",m 141 | self.plugins_time[m]=os.stat(file_name) 142 | mobj = self.import_module(m) 143 | if mobj is not None: 144 | self.load_class(mobj) 145 | # print 'items:',self.plugins_time.keys() 146 | #print 'have nokey:',map(self.plugins_time.has_key,self.plugins.keys()) 147 | pass 148 | def dispatch(self, from_uin, content, type, callback): 149 | """ 调度插件处理消息 150 | """ 151 | 152 | try: 153 | for key, val in self.plugins.items(): 154 | if val.is_match(from_uin, content, type): 155 | try: 156 | val.handle_message(callback) 157 | logger.info(u"Plugin {0} handled message {1}".format(key, content)) 158 | except: 159 | logger.error(u"Plugin {0} was encoutered an error" 160 | .format(key), exc_info = True) 161 | else: 162 | pass #return True 163 | return False 164 | except Exception,e: 165 | logger.error(u"dispatch {0} was encoutered an error",e) 166 | 167 | if __name__ == "__main__": 168 | print 'test' 169 | p=PluginLoader(None) 170 | print 'modules:',p.list_modules() 171 | print 'plugins:',p.plugins,p.plugins_time 172 | 173 | pass 174 | -------------------------------------------------------------------------------- /plugins/_linktitle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | __desc__ = 'Fetch link title or info' 4 | 5 | from functools import partial 6 | import logging 7 | 8 | from _fetchtitle import ( 9 | TitleFetcher, MediaType, 10 | GithubFinder, GithubUserFinder, 11 | URLFinder, 12 | ) 13 | 14 | from tornado.httpclient import AsyncHTTPClient 15 | from tornado.ioloop import IOLoop 16 | 17 | httpclient = AsyncHTTPClient() 18 | 19 | try: 20 | import regex as re 21 | # modified from http://daringfireball.net/2010/07/improved_regex_for_matching_urls 22 | # This may take too long for builtin regex to match e.g. 23 | # https://wiki.archlinux.org/index.php/USB_Installation_Media_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87) 24 | # This won't match http://[::1] 25 | link_re = re.compile(r'''\b(?:https?://|www\.|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\((?:[^\s()<>]+|\([^\s()<>]+\))*\))+(?:\((?:[^\s()<>]+|\([^\s()<>]+\))*\)|[^\s`!()\[\]{};:'".,<>??«»“”‘’)】。,])''', re.ASCII | re.I) 26 | except ImportError: 27 | import re 28 | logging.warn('mrab regex module not available, using simpler URL regex.') 29 | link_re = re.compile(r'\b(?:https?://|www\.)[-A-Z0-9+&@#/%=~_|$?!:,.]*[A-Z0-9+&@#/%=~_|$]') 30 | 31 | 32 | _black_list = ( 33 | r'p\.vim-cn\.com/\w{3}/?', 34 | r'^http://p\.gocmd\.net/\w{3}/?', 35 | r'^http://paste\.edisonnotes\.com/\w{3}/?', 36 | r'paste\.ubuntu\.(?:org\.cn|com)/\d+/?$', 37 | r'(?:imagebin|susepaste|paste\.kde)\.org/\d+/?$', 38 | r'^https?://(?:gitcafe|geakit)\.com/', 39 | r'^http://ideone\.com/\w+$', 40 | r'^http://imgur\.com/\w+$', 41 | r'^https?://github\.com/\w+/\w+/(?!issues/|commit/).+', 42 | r'\$', 43 | r'code\.bulix\.org', 44 | r'slexy.org/view/\w+$', 45 | r'paste\.opensuse\.org/\d+$', 46 | r'^http://paste\.linuxzen\.com/show/\d+$', 47 | r'^http://paste\.fedoraproject\.org/', 48 | r'^http://pastebin\.centos\.org/\d+/?$', 49 | r'^http://fpaste.org/\w+/', 50 | r'^http://supercat-lab\.org/pastebox/.+', 51 | r'^https://groups\.google\.com/forum/#', 52 | r'^http://paste\.linuxzen\.com/p/', 53 | r'^http://0\.web\.qstatic\.com/webqqpic/style/face/' 54 | r'^http://127\.0\.0\.1' 55 | ) 56 | 57 | _black_list = tuple(re.compile(x) for x in _black_list) 58 | 59 | _stop_url_pairs = ( 60 | ('http://weibo.com/signup/', 'http://weibo.com/'), 61 | ('http://weibo.com/signup/', 'http://www.weibo.com/'), 62 | ('http://weibo.com/login.php?url=', 'http://weibo.com/'), 63 | ('http://weibo.com/login.php?url=', 'http://www.weibo.com/'), 64 | ('https://accounts.google.com/ServiceLogin?', 'https://www.google.com/'), 65 | ('https://accounts.google.com/ServiceLogin?', 'https://plus.google.com/'), 66 | ('https://accounts.google.com/ServiceLogin?', 'https://accounts.google.com/'), 67 | ('https://bitbucket.org/account/signin/', 'https://bitbucket.org/'), 68 | ('http://www.renren.com/SysHome.do?origURL=', 'http://www.renren.com/'), 69 | ) 70 | def filesize(size): 71 | if size < 1024: 72 | num, unit = size, "B" 73 | 74 | elif size > 1024 and size < 1024 ** 2 : 75 | num, unit = size / 1024, "KB" 76 | elif size > 1024 ** 2 and size < 1024 ** 3: 77 | num, unit = size / (1024 ** 2), "MB" 78 | elif size > 1024 ** 3 and size < 1024 ** 4: 79 | num, unit = size / (1024 ** 3), "G" 80 | else: 81 | num, unit = size / (1024 ** 4), "T" 82 | 83 | return u"{0} {1}".format(num, unit) 84 | 85 | def blacklisted(u): 86 | for i in _black_list: 87 | if i.search(u): 88 | return True 89 | return False 90 | 91 | class StopURLs(URLFinder): 92 | @classmethod 93 | def _match_url(cls, url, fetcher): 94 | for login, origin in _stop_url_pairs: 95 | if url.startswith(login): 96 | last_url = fetcher.url_visited[-1] 97 | if last_url.startswith(origin): 98 | return True 99 | 100 | def __call__(self): 101 | self.done(False) 102 | 103 | class SogouImage(URLFinder): 104 | _url_pat = re.compile(r'http://pinyin\.cn/.+$') 105 | _img_pat = re.compile(br'"http://input\.shouji\.sogou\.com/multimedia/[^.]+\.jpg"') 106 | def __call__(self): 107 | httpclient.fetch(self.fullurl, self._got_page) 108 | 109 | def _got_page(self, res): 110 | m = self._img_pat.search(res.body) 111 | if m: 112 | url = self.url = m.group()[1:-1].decode('latin1') 113 | call_fetcher(url, self._got_image, referrer=self.fullurl) 114 | 115 | def _got_image(self, info, fetcher): 116 | self.done(info) 117 | 118 | def format_github_repo(repoinfo): 119 | if not repoinfo['description']: 120 | repoinfo['description'] = u'该仓库没有描述 :-(' 121 | ans = u'⇪Github 项目描述:%(description)s (%(language)s) ♡ %(watchers)d ⑂ %(forks)d,最后更新:%(updated_at)s' % repoinfo 122 | if repoinfo['fork']: 123 | ans += ' (forked)' 124 | ans += u'。' 125 | return ans 126 | 127 | def prepare_field(d, key, prefix): 128 | d[key] = prefix + d[key] if d.get(key, False) else '' 129 | 130 | def format_github_user(userinfo): 131 | prepare_field(userinfo, u'blog', u',博客:') 132 | prepare_field(userinfo, u'company', u',公司:') 133 | prepare_field(userinfo, u'location', u',地址:') 134 | if 'name' not in userinfo: 135 | userinfo['name'] = userinfo['login'] 136 | ans = u'⇪Github %(type)s:%(name)s,%(public_repos)d 公开仓库,%(followers)d 关注者,关注 %(following)d 人%(blog)s %(company)s%(location)s,最后活跃时间:%(updated_at)s。' % userinfo 137 | return ans 138 | 139 | def format_mediatype(info): 140 | ret = u'⇪文件类型: ' + info.type 141 | if info.size: 142 | ret += u', 文件大小: ' + filesize(info.size) 143 | if info.dimension: 144 | s = info.dimension 145 | if isinstance(s, tuple): 146 | s = u'%dx%d' % s 147 | ret += u', 图像尺寸: ' + s 148 | return ret 149 | 150 | def replylinktitle(reply, info, fetcher): 151 | if isinstance(info, bytes): 152 | try: 153 | info = info.decode('gb18030') 154 | except UnicodeDecodeError: 155 | pass 156 | 157 | timeout = None 158 | finderC = fetcher.finder.__class__ 159 | if info is False: 160 | logging.info('url skipped: %s', fetcher.origurl) 161 | return 162 | elif finderC is SogouImage: 163 | print(info) 164 | ans = u'⇪搜索输入法图片: %s' % format_mediatype(info)[3:] 165 | elif isinstance(info, basestring): 166 | if fetcher.status_code != 200: 167 | info = '[%d] ' % fetcher.status_code + info 168 | ans = u'⇪网页标题: ' + info.replace('\n', '') 169 | elif isinstance(info, MediaType): 170 | ans = format_mediatype(info) 171 | elif info is None: 172 | ans = u'该网页没有标题 :-(' 173 | elif isinstance(info, dict): # github json result 174 | res = fetcher.finder.response 175 | if res.code != 200: 176 | logging.warn(u'Github{,User}Finder returned HTTP code %s (body is %s).', res.code, res.body) 177 | ans = u'[Error %d]' % res.code 178 | else: 179 | if finderC is GithubFinder: 180 | ans = format_github_repo(info) 181 | elif finderC is GithubUserFinder: 182 | ans = format_github_user(info) 183 | else: 184 | logging.error(u'got a dict of unknown type: %s', finderC.__name__) 185 | ans = u'(内部错误)' 186 | else: 187 | ans = u'出错了! {0}'.format(info) 188 | 189 | if fetcher.origurl != fetcher.fullurl: 190 | ans += u' (重定向到 %s )' % fetcher.fullurl 191 | 192 | logging.info(u'url info: %s', ans) 193 | reply(ans) 194 | 195 | def call_fetcher(url, callback, referrer=None): 196 | fetcher = TitleFetcher(url, callback, referrer=referrer, url_finders=( 197 | GithubFinder, GithubUserFinder, SogouImage, StopURLs), run_at_init=False) 198 | try: 199 | fetcher.run() 200 | except UnicodeError as e: 201 | callback(e, fetcher) 202 | 203 | def getTitle(u, reply): 204 | logging.info('fetching url: %s', u) 205 | call_fetcher(u, partial(replylinktitle, reply)) 206 | 207 | 208 | def get_urls(msg): 209 | seen = set() 210 | for m in link_re.finditer(msg): 211 | u = m.group(0) 212 | if u not in seen: 213 | if blacklisted(u): 214 | continue 215 | if not u.startswith("http"): 216 | if msg[m.start() - 3: m.start()] == '://': 217 | continue 218 | u = 'http://' + u 219 | if u in seen: 220 | continue 221 | if u.count('/') == 2: 222 | u += '/' 223 | if u in seen: 224 | continue 225 | seen.add(u) 226 | return seen 227 | 228 | def fetchtitle(urls, reply): 229 | for u in urls: 230 | getTitle(u, reply) 231 | 232 | def register(bot): 233 | bot.register_msg_handler(fetchtitle) 234 | 235 | 236 | if __name__ == "__main__": 237 | def cb(tt): 238 | print tt 239 | fetchtitle(["http://www.baidu.com"], cb) 240 | IOLoop.instance().start() 241 | # vim:se sw=2: 242 | -------------------------------------------------------------------------------- /twqq/tornadohttpclient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 13/08/05 10:49:47 7 | # Desc : 8 | # 9 | from __future__ import print_function 10 | 11 | import os 12 | import time 13 | import pycurl 14 | import mimetools 15 | import mimetypes 16 | import itertools 17 | import threading 18 | try: 19 | from cookielib import Cookie, CookieJar 20 | except ImportError: 21 | from http.cookiejar import Cookie, CookieJar #py3 22 | 23 | try: 24 | from urllib import urlencode 25 | except ImportError: 26 | from urllib.parse import urlencode 27 | 28 | from functools import partial 29 | 30 | from tornado.curl_httpclient import CurlAsyncHTTPClient 31 | from tornado.ioloop import IOLoop 32 | 33 | 34 | class TornadoHTTPClient(CurlAsyncHTTPClient): 35 | def initialize(self, *args, **kwargs): 36 | super(TornadoHTTPClient, self).initialize(*args, **kwargs) 37 | self._cookie = {} 38 | self._proxy = {} 39 | self._user_agent = None 40 | self.keep_alive = True 41 | self.use_cookie = True 42 | self.debug = False 43 | self.validate_cert = True 44 | self._headers = {} 45 | 46 | self._share = pycurl.CurlShare() 47 | self._share.setopt(pycurl.SH_SHARE, pycurl.LOCK_DATA_COOKIE) 48 | self._share.setopt(pycurl.SH_SHARE, pycurl.LOCK_DATA_DNS) 49 | self.setup_curl() 50 | 51 | 52 | def setup_curl(self): 53 | _curls = [] 54 | for curl in self._curls: 55 | curl.setopt(pycurl.SHARE, self._share) 56 | if self.use_cookie: 57 | curl.setopt(pycurl.COOKIEFILE, "cookie") 58 | curl.setopt(pycurl.COOKIEJAR, "cookie_jar") 59 | 60 | _curls.append(curl) 61 | self._curls = _curls 62 | 63 | 64 | def set_user_agent(self, user_agent): 65 | self._user_agent = user_agent 66 | 67 | 68 | def set_global_headers(self, headers): 69 | self._headers = headers 70 | 71 | 72 | def set_proxy(self, host, port = 8080, username = None, password = None): 73 | assert isinstance(port, (int, long)) 74 | self._proxy["proxy_host"] = host 75 | self._proxy["proxy_port"] = port 76 | if username: 77 | self._proxy["proxy_username"] = username 78 | 79 | if password: 80 | self._proxy["proxy_password"] = password 81 | 82 | 83 | def unset_proxy(self): 84 | self._proxy = {} 85 | 86 | 87 | def wrap_prepare_curl_callback(self, callback): 88 | def _wrap_prepare_curl_callback(curl): 89 | if self.debug: 90 | curl.setopt(pycurl.VERBOSE, 1) 91 | 92 | if callback: 93 | return callback(curl) 94 | return _wrap_prepare_curl_callback 95 | 96 | 97 | def wrap_callback(self, callback, args = (), kwargs = {}): 98 | return partial(callback, *args, **kwargs) 99 | 100 | 101 | def fetch(self, request, callback, **kwargs): 102 | delay = kwargs.pop("delay", 0) 103 | if delay: 104 | t = threading.Thread(target = self._fetch, 105 | args = (request, callback, kwargs, delay)) 106 | t.setDaemon(True) 107 | t.start() 108 | return 109 | 110 | curl_callback = kwargs.pop("prepare_curl_callback", None) 111 | curl_callback = self.wrap_prepare_curl_callback(curl_callback) 112 | 113 | kwargs.update(self._proxy) 114 | self._user_agent and kwargs.update(user_agent = self._user_agent) 115 | kwargs.update(prepare_curl_callback = curl_callback) 116 | 117 | args, kw = kwargs.pop("args", ()), kwargs.pop("kwargs", {}) 118 | if callable(callback): 119 | callback = self.wrap_callback(callback, args, kw) 120 | 121 | 122 | headers = kwargs.pop("headers", {}) 123 | headers.update(self._headers) 124 | self.keep_alive and headers.update(Connection = "keep-alive") 125 | kwargs.update(validate_cert = self.validate_cert) 126 | 127 | super(TornadoHTTPClient, self).fetch(request, callback, headers = headers, **kwargs) 128 | 129 | 130 | def _fetch(self, request, callback, kwargs, delay): 131 | if isinstance(threading.currentThread(), threading._MainThread): 132 | raise threading.ThreadError, "Can't run this function in _MainThread" 133 | time.sleep(delay) 134 | self.fetch(request, callback, **kwargs) 135 | 136 | 137 | def post(self, url, params = {}, callback = None, **kwargs): 138 | kwargs.pop("method", None) 139 | kwargs.update(body = urlencode(params)) 140 | self.fetch(url, callback, method="POST", **kwargs) 141 | 142 | 143 | def get(self, url, params = {}, callback = None, **kwargs): 144 | kwargs.pop("method", None) 145 | url = "{0}?{1}".format(url, urlencode(params)) 146 | self.fetch(url, callback, **kwargs) 147 | 148 | 149 | def upload(self, url, field, path, params = {}, mimetype = None, 150 | callback = None, **kwargs): 151 | method = kwargs.pop("method", "POST") 152 | form = UploadForm() 153 | [form.add_field(name, value) for name, value in params.items()] 154 | _, fname = os.path.split(path) 155 | form.add_file(field, fname, open(path, 'r'), mimetype) 156 | kwargs.update(body = str(form)) 157 | kwargs.update(method = method) 158 | kwargs.update(headers = {"Content-Type":form.get_content_type()}) 159 | self.fetch(url, callback, **kwargs) 160 | 161 | 162 | @property 163 | def cookie(self): 164 | lst = [] 165 | for curl in self._curls: 166 | lst.extend(curl.getinfo(pycurl.INFO_COOKIELIST)) 167 | 168 | return self._parse_cookie(lst) 169 | 170 | def _parse_cookie(self, lst): 171 | for item in lst: 172 | domain, domain_specified, path, path_specified, expires,\ 173 | name, value = item.split("\t") 174 | 175 | cookie = Cookie(0, name, value, None, False, domain, 176 | domain_specified.lower() == "true", 177 | domain.startswith("."), path, 178 | path_specified.lower() == "true", False, expires, 179 | False, None, None, {}) 180 | 181 | self._cookie.update({domain:{path:{name:cookie}}}) 182 | 183 | return self._cookie 184 | 185 | 186 | @property 187 | def cookiejar(self): 188 | cookiejar = CookieJar() 189 | for domain, items in self._cookie.items(): 190 | for path, names in items.items(): 191 | for name, cookie in names.items(): 192 | cookiejar.set_cookie(cookie) 193 | 194 | return cookiejar 195 | 196 | 197 | def start(self): 198 | IOLoop.instance().start() 199 | 200 | 201 | def stop(self): 202 | IOLoop.instance().stop() 203 | 204 | 205 | class UploadForm(object): 206 | def __init__(self): 207 | self.form_fields = [] 208 | self.files = [] 209 | self.boundary = mimetools.choose_boundary() 210 | self.content_type = 'multipart/form-data; boundary=%s' % self.boundary 211 | return 212 | 213 | def get_content_type(self): 214 | return self.content_type 215 | 216 | def add_field(self, name, value): 217 | self.form_fields.append((str(name), str(value))) 218 | return 219 | 220 | def add_file(self, fieldname, filename, fileHandle, mimetype=None): 221 | body = fileHandle.read() 222 | if mimetype is None: 223 | mimetype = ( mimetypes.guess_type(filename)[0] 224 | or 225 | 'applicatioin/octet-stream') 226 | self.files.append((fieldname, filename, mimetype, body)) 227 | return 228 | 229 | def __str__(self): 230 | parts = [] 231 | part_boundary = '--' + self.boundary 232 | 233 | parts.extend( 234 | [ part_boundary, 235 | 'Content-Disposition: form-data; name="%s"' % name, 236 | '', 237 | value, 238 | ] 239 | for name, value in self.form_fields) 240 | if self.files: 241 | parts.extend([ 242 | part_boundary, 243 | 'Content-Disposition: form-data; name="%s"; filename="%s"' %\ 244 | (field_name, filename), 245 | 'Content-Type: %s' % content_type, 246 | '', 247 | body, 248 | ] for field_name, filename, content_type, body in self.files) 249 | 250 | flattened = list(itertools.chain(*parts)) 251 | flattened.append('--' + self.boundary + '--') 252 | flattened.append('') 253 | return '\r\n'.join(flattened) 254 | 255 | 256 | 257 | if __name__ == "__main__": 258 | http = TornadoHTTPClient() 259 | http.debug = False 260 | def callback(response): 261 | print(response.headers) 262 | print(http.cookie) 263 | http.stop() 264 | 265 | http.get("http://www.baidu.com", callback = callback) 266 | try: 267 | http.start() 268 | except KeyboardInterrupt: 269 | print("exiting...") 270 | 271 | def show_cookie(): 272 | print(http.cookie) 273 | -------------------------------------------------------------------------------- /webqq.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Copyright 2013 cold 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # 19 | # Author : cold 20 | # E-mail : wh_linux@126.com 21 | # Date : 13/11/14 13:23:49 22 | # Desc : 23 | # 24 | from __future__ import print_function 25 | 26 | import os 27 | import sys 28 | import time 29 | import atexit 30 | import smtplib 31 | import logging 32 | import traceback 33 | 34 | from functools import partial 35 | from email.mime.text import MIMEText 36 | 37 | 38 | from client import WebQQClient 39 | from requests import kick_message_handler, PollMessageRequest 40 | from requests import system_message_handler, group_message_handler 41 | from requests import buddy_message_handler, BeforeLoginRequest 42 | from requests import register_request_handler, BuddyMsgRequest 43 | from requests import Login2Request, FriendInfoRequest 44 | from requests import sess_message_handler, discu_message_handler 45 | 46 | import config 47 | 48 | from server import http_server_run 49 | from plugins import PluginLoader 50 | 51 | 52 | logger = logging.getLogger("client") 53 | 54 | SMTP_HOST = getattr(config, "SMTP_HOST", None) 55 | 56 | 57 | def send_notice_email(): 58 | """ 发送提醒邮件 59 | """ 60 | if not SMTP_HOST: 61 | return False 62 | 63 | postfix = ".".join(SMTP_HOST.split(".")[1:]) 64 | me = "bot<{0}@{1}>".format(config.SMTP_ACCOUNT, postfix) 65 | 66 | msg = MIMEText(""" 你的WebQQ机器人需要一个验证码, 67 | 请打开你的服务器输入验证码: 68 | http://{0}:{1}""".format(config.HTTP_LISTEN, 69 | config.HTTP_PORT), 70 | _subtype="plain", _charset="utf-8") 71 | msg['Subject'] = u"WebQQ机器人需要验证码" 72 | msg["From"] = me 73 | msg['To'] = config.EMAIL 74 | try: 75 | server = smtplib.SMTP() 76 | server.connect(SMTP_HOST) 77 | server.login(config.SMTP_ACCOUNT, config.SMTP_PASSWORD) 78 | server.sendmail(me, [config.EMAIL], msg.as_string()) 79 | server.close() 80 | return True 81 | except Exception as e: 82 | traceback.print_exc() 83 | return False 84 | 85 | 86 | class Client(WebQQClient): 87 | verify_img_path = None 88 | message_requests = {} 89 | start_time = time.time() 90 | msg_num = 0 91 | 92 | def handle_verify_code(self, path, r, uin): 93 | self.verify_img_path = path 94 | 95 | if getattr(config, "UPLOAD_CHECKIMG", False): 96 | logger.info(u"正在上传验证码...") 97 | res = self.hub.upload_file("check.jpg", self.hub.checkimg_path) 98 | logger.info(u"验证码已上传, 地址为: {0}".format(res.read())) 99 | 100 | if getattr(config, "HTTP_CHECKIMG", False): 101 | if hasattr(self, "handler") and self.handler: 102 | self.handler.r = r 103 | self.handler.uin = uin 104 | 105 | logger.info("请打开 http://{0}:{1} 输入验证码" 106 | .format(config.HTTP_LISTEN, config.HTTP_PORT)) 107 | if getattr(config, "EMAIL_NOTICE", False): 108 | if send_notice_email(): 109 | logger.info("发送通知邮件成功") 110 | else: 111 | logger.warning("发送通知邮件失败") 112 | else: 113 | logger.info(u"验证码本地路径为: {0}".format(self.hub.checkimg_path)) 114 | check_code = None 115 | while not check_code: 116 | check_code = raw_input("输入验证码: ") 117 | self.enter_verify_code(check_code, r, uin) 118 | 119 | def enter_verify_code(self, code, r, uin, callback=None): 120 | super(Client, self).enter_verify_code(code, r, uin) 121 | self.verify_callback = callback 122 | self.verify_callback_called = False 123 | 124 | @register_request_handler(BeforeLoginRequest) 125 | def handle_verify_check(self, request, resp, data): 126 | if not data: 127 | self.handle_verify_callback(False, "没有数据返回验证失败, 尝试重新登录") 128 | return 129 | 130 | args = request.get_back_args(data) 131 | scode = int(args[0]) 132 | if scode != 0: 133 | self.handle_verify_callback(False, args[4]) 134 | 135 | def handle_verify_callback(self, status, msg=None): 136 | if not hasattr(self, "plug_loader"): 137 | self.plug_loader = PluginLoader(self) 138 | 139 | if hasattr(self, "verify_callback") and callable(self.verify_callback)\ 140 | and not self.verify_callback_called: 141 | self.verify_callback(status, msg) 142 | self.verify_callback_called = True 143 | 144 | @register_request_handler(Login2Request) 145 | def handle_login_errorcode(self, request, resp, data): 146 | if not resp.body: 147 | return self.handle_verify_callback(False, u"没有数据返回, 尝试重新登录") 148 | 149 | if data.get("retcode") != 0: 150 | return self.handle_verify_callback(False, u"登录失败: {0}" 151 | .format(data.get("retcode"))) 152 | 153 | @register_request_handler(FriendInfoRequest) 154 | def handle_frind_info_erro(self, request, resp, data): 155 | if not resp.body: 156 | self.handle_verify_callback(False, u"获取好友列表失败") 157 | return 158 | 159 | if data.get("retcode") != 0: 160 | self.handle_verify_callback(False, u"好友列表获取失败: {0}" 161 | .format(data.get("retcode"))) 162 | return 163 | self.handle_verify_callback(True) 164 | 165 | @kick_message_handler 166 | def handle_kick(self, message): 167 | self.hub.relogin() 168 | 169 | @system_message_handler 170 | def handle_friend_add(self, mtype, from_uin, account, message): 171 | if mtype == "verify_required": 172 | if getattr(config, "AUTO_ACCEPT", True): 173 | self.hub.accept_verify(from_uin, account, str(account)) 174 | 175 | @group_message_handler 176 | def handle_group_message(self, member_nick, content, group_code, 177 | send_uin, source): 178 | callback = partial(self.send_group_with_nick, member_nick, group_code) 179 | self.handle_message(send_uin, content, callback) 180 | 181 | @sess_message_handler 182 | def handle_sess_message(self, qid, from_uin, content, source): 183 | callback = partial(self.hub.send_sess_msg, qid, from_uin) 184 | self.handle_message(from_uin, content, callback, 's') 185 | 186 | @discu_message_handler 187 | def handle_discu_message(self, did, from_uin, content, source): 188 | nick = self.hub.get_friend_name(from_uin) 189 | callback = partial(self.send_discu_with_nick, nick, did) 190 | self.handle_message(from_uin, content, callback, 'g') 191 | 192 | def send_discu_with_nick(self, nick, did, content): 193 | content = u"{0}: {1}".format(nick, content) 194 | self.hub.send_discu_msg(did, content) 195 | 196 | def handle_message(self, from_uin, content, callback, type="g"): 197 | content = content.strip() 198 | if self.plug_loader.dispatch(from_uin, content, type, callback): 199 | self.msg_num += 1 200 | 201 | def send_group_with_nick(self, nick, group_code, content): 202 | content = u"{0}: {1}".format(nick, content) 203 | self.hub.send_group_msg(group_code, content) 204 | 205 | @buddy_message_handler 206 | def handle_buddy_message(self, from_uin, content, source): 207 | callback = partial(self.hub.send_buddy_msg, from_uin) 208 | self.handle_message(from_uin, content, callback, 'b') 209 | 210 | @register_request_handler(PollMessageRequest) 211 | def handle_qq_errcode(self, request, resp, data): 212 | if data and data.get("retcode") in [100006]: 213 | logger.error(u"获取登出消息 {0!r}".format(data)) 214 | self.hub.relogin() 215 | 216 | if data and data.get("retcode") in [103, 100002]: # 103重新登陆不成功, 暂时退出 217 | logger.error(u"获取登出消息 {0!r}".format(data)) 218 | exit() 219 | 220 | def send_msg_with_markname(self, markname, message, callback=None): 221 | request = self.hub.send_msg_with_markname(markname, message) 222 | if request is None: 223 | callback(False, u"不存在该好友") 224 | 225 | self.message_requests[request] = callback 226 | 227 | @register_request_handler(BuddyMsgRequest) 228 | def markname_message_callback(self, request, resp, data): 229 | callback = self.message_requests.get(request) 230 | if not callback: 231 | return 232 | 233 | if not data: 234 | callback(False, u"服务端没有数据返回") 235 | return 236 | 237 | if data.get("retcode") != 0: 238 | callback(False, u"发送失败, 错误代码:".format(data.get("retcode"))) 239 | return 240 | 241 | callback(True) 242 | 243 | def run(self, handler=None): 244 | self.handler = handler 245 | super(Client, self).run() 246 | 247 | 248 | def run_daemon(callback, args=(), kwargs = {}): 249 | path = os.path.abspath(os.path.dirname(__file__)) 250 | 251 | def _fork(num): 252 | try: 253 | pid = os.fork() 254 | if pid > 0: 255 | sys.exit(0) 256 | except OSError as e: 257 | sys.stderr.write("fork #%d faild:%d(%s)\n" % (num, e.errno, 258 | e.strerror)) 259 | sys.exit(1) 260 | 261 | _fork(1) 262 | 263 | os.setsid() 264 | # os.chdir("/") 265 | os.umask(0) 266 | 267 | _fork(2) 268 | pp = os.path.join(path, "pid.pid") 269 | 270 | with open(pp, 'w') as f: 271 | f.write(str(os.getpid())) 272 | 273 | lp = os.path.join(path, "log.log") 274 | lf = open(lp, 'a') 275 | os.dup2(lf.fileno(), sys.stdout.fileno()) 276 | os.dup2(lf.fileno(), sys.stderr.fileno()) 277 | callback(*args, **kwargs) 278 | 279 | def _exit(): 280 | os.remove(pp) 281 | lf.close() 282 | 283 | atexit.register(_exit) 284 | 285 | 286 | def main(): 287 | webqq = Client(config.QQ, config.QQ_PWD, 288 | debug=getattr(config, "TRACE", False)) 289 | try: 290 | if getattr(config, "HTTP_CHECKIMG", False): 291 | http_server_run(webqq) 292 | else: 293 | webqq.run() 294 | except KeyboardInterrupt: 295 | print("Exiting...", file=sys.stderr) 296 | except SystemExit: 297 | logger.error("检测到退出, 重新启动") 298 | os.execv(sys.executable, [sys.executable] + sys.argv) 299 | 300 | 301 | if __name__ == "__main__": 302 | import tornado.log 303 | from tornado.options import options 304 | 305 | if not getattr(config, "DEBUG", False): 306 | options.log_file_prefix = getattr(config, "LOG_PATH", "log.log") 307 | options.log_file_max_size = getattr( 308 | config, "LOG_MAX_SIZE", 5 * 1024 * 1024) 309 | options.log_file_num_backups = getattr(config, "LOG_BACKUPCOUNT", 10) 310 | tornado.log.enable_pretty_logging(options=options) 311 | 312 | if not config.DEBUG and hasattr(os, "fork"): 313 | run_daemon(main) 314 | else: 315 | main() 316 | -------------------------------------------------------------------------------- /plugins/smartrobot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : rootntsd 5 | # E-mail : rootntsd@gmail.com 6 | # Date : 14/01/26 0:35:00 7 | # Desc : ''' 8 | # 9 | #from __init__ import BasePlugin 10 | from plugins import BasePlugin 11 | 12 | import config 13 | from bs4 import BeautifulSoup 14 | import random 15 | 16 | from tornadohttpclient import TornadoHTTPClient 17 | try: 18 | import sys 19 | reload(sys) 20 | sys.setdefaultencoding('utf8') 21 | except Exception,e: 22 | print 'except',e 23 | 24 | class Searcher(object): 25 | 26 | def __init__(self, http = None): 27 | self.http = http or TornadoHTTPClient() 28 | #self.http.debug=True 29 | self.result=[] 30 | self.statment=[] 31 | self.helpkeyword=['?',u'怎么',u'什么',u'鸭子','ee',u'好了','YY','yy',u'神马',u'啊',u'?',u'是么',u'依依',u'EE',u'BSD鸭子',u'能不能',u'多少',u'么'] 32 | pass 33 | def parse(self,content): 34 | self.statment.append(content.split('?')) 35 | return len(self.statment) 36 | def search(self,content): 37 | count=self.parse(content) 38 | for c in self.statment: 39 | t=''.join(c) 40 | print 'statment:',t 41 | self.result.append(self.baidu_know_search(t)) 42 | print 'after:',self.result 43 | print 'count:',count 44 | return count 45 | def get_result(self): 46 | print 'get_result:',type(self.result),len(self.result) 47 | ret='' 48 | if len(self.result)>0: 49 | number=random.randint(0, len(self.result)-1) 50 | print 'number:',number,'len:',len(self.result)-1 51 | rets=self.result[number] 52 | if len(rets)>0: 53 | ret=''.join(rets[0]) 54 | print 'ret:',ret,type(ret) 55 | return ret 56 | return ret 57 | def baidu_search(self,content): 58 | try: 59 | print 'baidu_search' 60 | result=[] 61 | def getdata(response): 62 | #print(response.headers) 63 | #print(self.http.cookie) 64 | #print 'body:',response.body 65 | #print 'error',response.error 66 | body=response.body 67 | 68 | if len(body)<=10: 69 | return 70 | #soup = BeautifulSoup(body,'html5lib') #do not enable ,'html5lib' 71 | soup = BeautifulSoup(body) 72 | 73 | #print(soup.prettify()) 74 | #for link in soup.find_all('a'): 75 | # print(link.get('href')) 76 | #print(soup.get_text()) 77 | '''find normal''' 78 | #tds=soup.find_all('td',class_='c-default') #why no work in linux i dont know why ,who can answer? 79 | tds=soup.find_all('div',attrs={'class':'result c-container'}) 80 | print type(tds) 81 | print len(tds) 82 | for i in range(len(tds)): 83 | h3=tds[i].find('h3') 84 | div=tds[i].find_all('div',attrs={'class':'c-abstract'}) 85 | if h3!=None: 86 | print i,'h3:',h3.get_text() 87 | for d in div: 88 | print i,'txt:',d.get_text() 89 | result.append(d.get_text()) 90 | 91 | else: 92 | print i,'no:',tds[i].get_text() 93 | '''find baike''' 94 | tds=soup.find_all('div',attrs={'class':'result-op c-container xpath-log'}) 95 | print type(tds) 96 | print len(tds) 97 | for i in range(len(tds)): 98 | h3=tds[i].find('h3') 99 | div=tds[i].find_all('div') 100 | if h3!=None: 101 | print i,'h3:',h3.get_text() 102 | for d in div: 103 | print i,'txt:',d.get_text() 104 | else: 105 | print i,'no:',tds[i].get_text() 106 | 107 | # '''find new''' 108 | # tds=soup.find_all('div',id='content_left') 109 | # print '======:',len(tds),tds[0].get_text() 110 | 111 | # for td in tds: 112 | # #print td 113 | # h3=td.find('h3') 114 | # div=td.find_all('div',class_='c-abstract') 115 | # if h3!=None: 116 | # print 'h3:',h3.get_text() 117 | # for d in div: 118 | # print 'txt:',d.get_text() 119 | #for l in container: 120 | # print l 121 | #content_left=container.
122 | # tabs=soup.find_all('table',_class='result') 123 | # for d in tabs: 124 | # print 'h',d.get('h3') 125 | #self.http.stop() 126 | ran=0 127 | data='.....' 128 | if len(result)>0: 129 | ran=random.randint(0,len(result)-1) 130 | data=result[ran] 131 | print 'self.http.stop',len(result),' rand:',ran 132 | print 'send:',data 133 | if self.send_msg!=None: 134 | s=data 135 | s=s.lstrip(' ') 136 | s=s.lstrip('最佳答案:') 137 | s=s.lstrip('问题描述:') 138 | s=s.lstrip('最佳答案:') 139 | s=s.lstrip('问题描述:') 140 | s=s.rstrip('最佳答案:') 141 | s=s.rstrip('问题描述:') 142 | s=s.lstrip(' ') 143 | #s=s.split(' ')[0] 144 | 145 | self.send_msg(s) 146 | try: 147 | url="http://www.baidu.com/s?wd="+content 148 | self.http.get(url, callback = getdata) 149 | #self.http.start() 150 | except KeyboardInterrupt: 151 | print("exiting...") 152 | except Exception,e: 153 | print 'except',e 154 | return result 155 | except Exception,e: 156 | print 'except:',e 157 | 158 | def baidu_know_search(self,content): 159 | try: 160 | print 'baidu_know_search' 161 | result=[] 162 | def getdata(response): 163 | #print(response.headers) 164 | #print(self.http.cookie) 165 | #print 'body:',response.body 166 | #print 'error',response.error 167 | body=response.body 168 | 169 | if len(body)<=10: 170 | return 171 | #soup = BeautifulSoup(body,'html5lib') #do not enable ,'html5lib' 172 | soup = BeautifulSoup(body) 173 | 174 | #print(soup.prettify()) 175 | #for link in soup.find_all('a'): 176 | # print(link.get('href')) 177 | #print(soup.get_text()) 178 | '''find normal''' 179 | #tds=soup.find_all('td',class_='c-default') #why no work in linux i dont know why ,who can answer? 180 | dls=soup.find_all('div',attrs={'class':'list'}) 181 | print type(dls) 182 | print len(dls) 183 | for i in range(len(dls)): 184 | #answer=dls[i].find('h3') 185 | 186 | answer=dls[i].find_all('dd',attrs={'class':'dd answer'}) 187 | print 'type answer:',type(answer) 188 | print 'answer:',answer 189 | if answer!=None: 190 | for d in range(len(answer)): 191 | print 'answer d:',d , answer[d].get_text() 192 | result.append(answer[d].get_text()) 193 | 194 | else: 195 | print i,'no:',dls[i].get_text() 196 | 197 | 198 | 199 | # '''find new''' 200 | # tds=soup.find_all('div',id='content_left') 201 | # print '======:',len(tds),tds[0].get_text() 202 | 203 | # for td in tds: 204 | # #print td 205 | # h3=td.find('h3') 206 | # div=td.find_all('div',class_='c-abstract') 207 | # if h3!=None: 208 | # print 'h3:',h3.get_text() 209 | # for d in div: 210 | # print 'txt:',d.get_text() 211 | #for l in container: 212 | # print l 213 | #content_left=container.
214 | # tabs=soup.find_all('table',_class='result') 215 | # for d in tabs: 216 | # print 'h',d.get('h3') 217 | #self.http.stop() 218 | ran=0 219 | data='.....' 220 | if len(result)>0: 221 | ran=random.randint(0,len(result)-1) 222 | data=result[ran] 223 | print 'self.http.stop',len(result),' rand:',ran 224 | print 'send:',data 225 | if self.send_msg!=None: 226 | s=data 227 | s=s.lstrip('答:') 228 | #s=s.split(' ')[0] 229 | 230 | self.send_msg(s) 231 | try: 232 | url="http://zhidao.baidu.com/search?lm=0&rn=10&pn=0&fr=search&ie=utf8&word="+content 233 | self.http.get(url, callback = getdata) 234 | #self.http.start() 235 | except KeyboardInterrupt: 236 | print("exiting...") 237 | except Exception,e: 238 | print 'except',e 239 | return result 240 | except Exception,e: 241 | print 'except:',e 242 | 243 | def find(self,content): 244 | print 'find '+content 245 | if self.findKey(content): 246 | #print '找到' 247 | return True 248 | else: 249 | return False 250 | #print '没找到' 251 | return False 252 | def findKey(self,content): 253 | for key in self.helpkeyword: 254 | #print key 255 | if content.find(key)>-1: 256 | return True; 257 | return False 258 | 259 | class SmartRobotPlugin(BasePlugin): 260 | def get_result(self): 261 | return self.searcher.get_result() 262 | 263 | def handle_message(self, callback): 264 | # data=self.get_result() 265 | # #data='aa' 266 | # print 'send data:',data 267 | # callback(data) 268 | # print 'callback end' 269 | self.callback=callback 270 | self.searcher.send_msg=self.callback 271 | pass 272 | def is_match(self, from_uin, content,type): 273 | #print 'from_uin:',from_uin," content:",content," type:",type 274 | if not getattr(config, "SmartRobot_Enabled", False): 275 | return False 276 | else: 277 | self.searcher = Searcher() 278 | 279 | # print 'is match' 280 | if type == "g" or type=="s" or type=="b": 281 | print 'search' 282 | if self.searcher.find(content): 283 | result=self.searcher.search(content) 284 | if result>=0 : 285 | print 'result count:',result 286 | 287 | return True 288 | else: 289 | self.content = content 290 | return True 291 | return False 292 | pass 293 | if __name__ == "__main__": 294 | c=TornadoHTTPClient() 295 | #c.start() 296 | robot=SmartRobotPlugin(None,None,None,None) 297 | #while True: 298 | 299 | if robot.is_match(111, 'ss?', 'g')==True: 300 | data=robot.get_result() 301 | print "data:",data,type(data) 302 | else: 303 | print 'no found' 304 | c=TornadoHTTPClient() 305 | s=Searcher(c) 306 | c.start() 307 | #s.baidu_search('ss') 308 | c.stop() 309 | # 310 | pass 311 | 312 | -------------------------------------------------------------------------------- /twqq/objects.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 14/04/11 17:28:15 7 | # Desc : 将返回信息实例化 8 | # 9 | """ 对应WebQQ的数据结构抽象 10 | """ 11 | import logging 12 | 13 | logger = logging.getLogger("twqq") 14 | 15 | 16 | class UniqueIds(object): 17 | """ 唯一ID 18 | 为每个好友/群/讨论组/群成员都保持一个唯一标识 19 | """ 20 | T_FRI = 1 # 好友 21 | T_TMP = 0 # 临时(群成员) 22 | T_GRP = 4 # 群 23 | T_DIS = 3 # 讨论组 24 | 25 | _map = {} # 分配的id 到 uin和type的对应关系 26 | _r_map = {} # uin 和 id 的对应关系 27 | _last_id = 0 # 最后一个id, 保持唯一 28 | 29 | @classmethod 30 | def alloc(cls, uin, _type): 31 | """ 分配一个, uin 是webqq的唯一标识, _type 对应 组/好友/讨论组/群成员 32 | 并返回这个 id 33 | """ 34 | assert _type in [cls.T_FRI, cls.T_TMP, cls.T_GRP, cls.T_DIS] 35 | if uin in cls._r_map: 36 | return cls._r_map[uin] 37 | else: 38 | _id = cls._last_id 39 | cls._last_id += 1 40 | cls._map[_id] = (uin, _type) 41 | cls._r_map[uin] = _id 42 | return _id 43 | 44 | @classmethod 45 | def get(cls, _id): 46 | """ 根据 _id 获取 uin 和对应的类型 47 | """ 48 | return cls._map.get(_id, (None, None)) 49 | 50 | @classmethod 51 | def get_type(cls, uin): 52 | """ 根据 uin 判断该uin 的类型 53 | """ 54 | return cls._map.get(cls._r_map.get(uin), (None, None))[1] 55 | 56 | @classmethod 57 | def get_id(cls, uin): 58 | return cls._r_map.get(uin) 59 | 60 | 61 | class ObjectsBase(object): 62 | 63 | def __init__(self, **kw): 64 | for key, val in kw.items(): 65 | setattr(self, key, val) 66 | 67 | 68 | class GroupMInfo(ObjectsBase): 69 | 70 | """ 对应群成员信息中的 minfo 字段中的元素 71 | 72 | :param nick: 成员qq昵称 73 | :param province: 成员省份 74 | :param gender: 成员性别 75 | :param uin: uin 76 | :param contry: 国家 77 | :param city: 城市 78 | :param client_type: 客户端类型 79 | :param stat: 目测固定都给10 80 | :param mflag: 1 则是管理员, 0 是普通成员 81 | :param card: 群名片 82 | """ 83 | 84 | def __init__(self, nick, province, gender, uin, country, city, 85 | stat=None, client_type=None, mflag=None, card=None): 86 | self.nick = nick 87 | self.province = province 88 | self.gender = gender # 性别 89 | self.uin = uin 90 | self.country = country 91 | self.city = city 92 | 93 | self.stat = stat 94 | self.client_type = client_type 95 | self.card = card 96 | self._id = UniqueIds.alloc(uin, UniqueIds.T_TMP) 97 | 98 | def is_manager(self): 99 | return True if self.mflag == 1 else False 100 | 101 | 102 | class VipInfo(ObjectsBase): 103 | 104 | """ 对应群信息 vipinfo 字段 105 | 106 | :param vip_level: 会员等级 107 | :param u: 用户 uin 108 | :param is_vip: 是否是管理员 109 | """ 110 | 111 | def __init__(self, vip_level, u, is_vip): 112 | self.vip_level = vip_level 113 | self.u = u 114 | self.is_vip = True if is_vip == 1 else False 115 | 116 | 117 | class Group(ObjectsBase): 118 | def __init__(self, flag, name, gid, code, memo=None, fingermemo=None, 119 | createtime=None, level=None, owner=None, option=None, 120 | members=None): 121 | self.flag = flag 122 | self.name = name 123 | self.gid = gid 124 | self.code = code 125 | self.group = None 126 | 127 | self.memo = memo 128 | self.fingermemo = fingermemo 129 | self.createtime = createtime 130 | self.level = level 131 | self.owner = owner 132 | self.option = option 133 | self._uin_map = {} # 群成员 uin 映射 134 | self._uin_name_map = {} # 群成员昵称到uin的映射 135 | self._id = UniqueIds.alloc(code, UniqueIds.T_GRP) 136 | 137 | def update(self, **kw): 138 | for key, val in kw.items(): 139 | setattr(self, key, val) 140 | 141 | def set_group_detail(self, data): 142 | """ 设置组详细信息, 包括群成员信息, 等. 143 | """ 144 | for kw in data.get("minfo", []): 145 | nick, uin = kw.get("nick"), kw.get("uin") 146 | if nick is not None: 147 | tmp = GroupMInfo(**kw) 148 | self._uin_name_map[tmp.nick] = tmp.uin 149 | self._uin_map[tmp.uin] = tmp 150 | 151 | for item in data.get("cards", []): 152 | uin = item.get("muin") 153 | if uin in self._uin_map and item.get("card") is not None: 154 | self._uin_map[uin].card = item.get("card") 155 | self._uin_name_map[item.get("card")] = uin 156 | else: 157 | logger.warn(u"card info {0} not in map: {1!r}" 158 | .format(uin, item)) 159 | 160 | for item in data.get("stats", []): 161 | uin = item.get("uin") 162 | if uin in self._uin_map: 163 | self._uin_map[uin].stat = item.get("stat") 164 | self._uin_map[uin].client_type = item.get("client_type") 165 | else: 166 | logger.warn(u"stats info {0} not in map: {1!r}" 167 | .format(uin, item)) 168 | 169 | for item in data.get("vipinfo", []): 170 | u = item.get("u") 171 | if u in self._uin_map: 172 | self._uin_map[u].vipinfo = item 173 | else: 174 | logger.warn(u"vip info {0} not in map: {1!r}" 175 | .format(uin, item)) 176 | 177 | self.set_detail_info(**data.get("ginfo", {})) 178 | 179 | def __iter__(self): 180 | """ 迭代返回成员的 uin 181 | """ 182 | if hasattr(dict, "iterkeys"): 183 | return self._uin_map.iterkeys() 184 | else: 185 | return self._uin_map.keys() 186 | 187 | def set_detail_info(self, face, memo, fingermemo, code, createtime, 188 | flag, level, name, gid, owner, option, members, **kw): 189 | """ 组成员信息中对应 ginfo 中的元素 190 | 191 | :param face: 群头像 192 | :param memo: 193 | :param class: 群类型 194 | :param fingermemo: 195 | :param createtime: 创建时间戳 196 | :param flag: 197 | :param level: 198 | :param name: 群名称 199 | :param gid: 群hash码 200 | :param owner: 群主 uin 201 | :param option: 202 | :param members: 群成员 203 | """ 204 | self.face = face 205 | self.memo = memo 206 | self._class = kw["class"] 207 | self.fingermemo = self.fingermemo 208 | self.createtime = createtime 209 | self.flag = flag 210 | self.level = level 211 | self.name = name 212 | self.gid = gid 213 | self.owner = owner 214 | self.option = option 215 | 216 | for item in members: 217 | uin = item.get("muin") 218 | tmp = self._uin_map.get(uin) 219 | if tmp: 220 | tmp.mflag = item.get("mflag") 221 | 222 | def __repr__(self): 223 | return u""\ 224 | .format(self.name, len(self._uin_map.keys()), self.level) 225 | 226 | def __unicode__(self): 227 | return self.__repr__() 228 | 229 | def get_nickname(self, uin): 230 | """ 获取群成员的昵称 231 | """ 232 | r = self._uin_map.get(uin) 233 | if r: 234 | return r.nick 235 | 236 | def get_cardname(self, uin): 237 | """ 获取群成员群名片 238 | """ 239 | r = self._uin_map.get(uin) 240 | if r: 241 | return r.card 242 | 243 | def get_show_name(self, uin): 244 | """ 获取显示名, 群名片优先, 无则取昵称 245 | """ 246 | r = self._uin_map.get(uin) 247 | if r: 248 | return r.card if r.card else r.nick 249 | 250 | def get_member_info(self, uin): 251 | return self._uin_map.get(uin) 252 | 253 | def is_manager(self, uin): 254 | """ 判断用户是否是群管理员 255 | """ 256 | r = self._uin_map.get(uin) 257 | if r: 258 | return r.is_manager() 259 | 260 | 261 | class GroupList(ObjectsBase): 262 | """ 组列表抽象 263 | """ 264 | 265 | def __init__(self, data): 266 | self._gcode_map = {} 267 | self._gid_gcode_map = {} 268 | self._gcode_name_map = {} 269 | self.update(data) 270 | 271 | def update(self, data): 272 | for kw in data.get("gnamelist", []): 273 | gcode = kw.get("code") 274 | if gcode not in self._gcode_map: 275 | group = Group(**kw) 276 | self._gcode_map[gcode] = group 277 | self._gid_gcode_map[kw.get("gid")] = gcode 278 | self._gcode_name_map[kw.get("name")] = gcode 279 | else: 280 | self._gcode_map[gcode].update(**kw) 281 | self.gmasklist = data.get("gmasklist", []) 282 | self.gmarklist = data.get("gmarklist", []) 283 | 284 | @property 285 | def gnamelist(self): 286 | return self._gcode_map.values() 287 | 288 | def __repr__(self): 289 | return str([x.name for x in self._gcode_map.values()]) 290 | 291 | def __unicode__(self): 292 | return self.__repr__().decode("utf-8") 293 | 294 | def __iter__(self): 295 | """ 迭代返回群对象 296 | """ 297 | if hasattr(dict, "itervalues"): 298 | return self._gcode_map.itervalues() 299 | else: 300 | return self._gcode_map.values() 301 | 302 | @property 303 | def groups(self): 304 | return [x for x in self._gcode_map.values()] 305 | 306 | def get_gcodes(self): 307 | return [x.code for x in self._gcode_map.values()] 308 | 309 | def find_group(self, gcode): 310 | return self._gcode_map.get(gcode) 311 | 312 | def set_group_info(self, gcode, data): 313 | """ 设置群信息 314 | 315 | :param gcode: 组代码 316 | :param data: 数据 317 | """ 318 | item = self.find_group(gcode) 319 | if item: 320 | item.set_group_detail(data) 321 | 322 | def get_members(self, gcode): 323 | """ 获取指定组的成员信息 324 | """ 325 | item = self.find_group(gcode) 326 | if item: 327 | return item._uin_map.values() 328 | 329 | def get_member(self, gcode, uin): 330 | """ 获取指定群成员的信息 331 | """ 332 | item = self.find_group(gcode) 333 | if item: 334 | return item.get_member(uin) 335 | 336 | def get_group_name(self, gcode): 337 | item = self.find_group(gcode) 338 | if item: 339 | return item.name 340 | 341 | def get_member_nick(self, gcode, uin): 342 | item = self.find_group(gcode) 343 | if item: 344 | return item.get_show_name(uin) 345 | 346 | def get_gid(self, gcode): 347 | item = self.find_group(gcode) 348 | if item: 349 | return item.gid 350 | 351 | def get_gcode(self, gid): 352 | return self._gid_gcode_map.get(gid) 353 | 354 | 355 | class DiscuMemInfo(ObjectsBase): 356 | 357 | """ 讨论组成员信息 358 | 359 | :param uin: uin 360 | :param nick: 昵称 361 | """ 362 | 363 | def __init__(self, uin, nick, status=None, client_type=None): 364 | self.uin = uin 365 | self.nick = nick 366 | self.status = status 367 | self.client_type = client_type 368 | self._id = UniqueIds.alloc(uin, UniqueIds.T_TMP) 369 | 370 | 371 | class Discu(ObjectsBase): 372 | 373 | """ 讨论组信息抽象 374 | 375 | :param did: 讨论组id 376 | :param name: 讨论组名称 377 | """ 378 | 379 | def __init__(self, did, name, discu_owner=None, discu_name=None, 380 | info_seq=None, mem_list=None): 381 | self._uin_map = {} 382 | self.did = did 383 | self.name = name 384 | self.discu_name = discu_name, 385 | self.discu_owner = discu_owner 386 | self.info_seq = info_seq 387 | self.mem_list = mem_list 388 | self._id = UniqueIds.alloc(did, UniqueIds.T_DIS) 389 | 390 | def set_detail(self, info, mem_status, mem_info): 391 | self.discu_name = info.get("discu_name") 392 | self.discu_owner = info.get("discu_owner") 393 | self.info_seq = info.get("info_seq") 394 | for item in mem_info: 395 | self._uin_map[item["uin"]] = DiscuMemInfo(**item) 396 | 397 | for item in mem_status: 398 | tmp = self._uin_map.get(item["uin"]) 399 | if tmp: 400 | tmp.status = item.get("status") 401 | tmp.client_type = item.get("client_type") 402 | 403 | def get_mname(self, uin): 404 | item = self._uin_map.get(uin) 405 | if item: 406 | return item.nick 407 | 408 | 409 | class DiscuList(ObjectsBase): 410 | 411 | """ 讨论组列表 412 | """ 413 | 414 | def __init__(self, data): 415 | self._did_map = {} 416 | self._did_name_map = {} 417 | 418 | self.update(data) 419 | 420 | def update(self, data): 421 | for item in data.get("dnamelist"): 422 | did = item["did"] 423 | if did not in self._did_map: 424 | self._did_name_map[item["name"]] = item["did"] 425 | self._did_map[did] = Discu(item["did"], item["name"]) 426 | else: 427 | self._did_map[did].did = did 428 | self._did_map[did].name = item["name"] 429 | 430 | @property 431 | def dids(self): 432 | return self._did_map.keys() 433 | 434 | @property 435 | def discus(self): 436 | return [x for x in self._did_map.values()] 437 | 438 | def get_name(self, did): 439 | r = self._did_map.get(did) 440 | if r: 441 | return r.name 442 | 443 | def set_detail(self, did, data): 444 | self._did_map[did].set_detail(**data) 445 | 446 | def get_did(self, name): 447 | return self._did_name_map.get(name) 448 | 449 | def get_mname(self, did, uin): 450 | r = self._did_map.get(did) 451 | if r: 452 | return r.get_mname(uin) 453 | 454 | 455 | class FriendInfo(ObjectsBase): 456 | 457 | """ 好友信息抽象 458 | 459 | :param uin: 唯一标识 460 | :param face: 头像 461 | :param nick: 昵称 462 | :param birthday: 生日 463 | :param occpation: 464 | :param phone: 手机号 465 | :param allow: 466 | :param colleage: 大学 467 | :param uin: uin 468 | :param constel: 469 | :param blood: 血型 470 | :param homepage: 主页 471 | :param stat: 状态 472 | :param vip_info: 是否vip 473 | :param country: 国家 474 | :param city: 城市 475 | :param personal: 476 | :param shengxiao: 生肖 477 | :param email: 邮件 478 | :param client_type: 客户端类型 479 | :param province: 省份 480 | :param gender: 性别 481 | :param mobile: 手机 482 | :param markname: 备注名 483 | :param categories: 分类id 484 | :param account: QQ号 485 | """ 486 | def __init__(self, uin, face, flag, nick, birthday=None, 487 | occpation=None, phone=None, allow=None, colleage=None, 488 | constel=None, blood=None, homepage=None, status=None, 489 | vip_info=None, country=None, city=None, personal=None, 490 | shengxiao=None, email=None, client_type=None, province=None, 491 | gender=None, mobile=None, markname=None, categories=None, 492 | account=None): 493 | self.face = face 494 | self.flag = flag 495 | self.nick = nick 496 | self.uin = uin 497 | self.birthda = birthday 498 | self.occpation = occpation 499 | self.phone = phone 500 | self.allow = allow 501 | self.colleage = colleage 502 | self.constel = constel 503 | self.blood = blood 504 | self.homepage = homepage 505 | self.status = status 506 | self.vip_info = vip_info 507 | self.country = country 508 | self.city = city 509 | self.personal = personal 510 | self.shengxiao = shengxiao 511 | self.email = email 512 | self.client_type = client_type 513 | self.province = province 514 | self.gender = gender 515 | self.mobile = mobile 516 | self.markname = markname 517 | self.categories = categories 518 | self.account = account 519 | self._id = UniqueIds.alloc(uin, UniqueIds.T_FRI) 520 | 521 | def update(self, **kwargs): 522 | for key, val in kwargs.items(): 523 | setattr(self, key, val) 524 | 525 | def set_markname(self, markname): 526 | self.markname = markname 527 | 528 | def set_categories(self, categories): 529 | self.categories = categories 530 | 531 | def set_vipinfo(self, vipinfo): 532 | self.vip_info = vipinfo 533 | 534 | def set_detail(self, **kw): 535 | for k, v in kw.items(): 536 | setattr(self, k, v) 537 | 538 | 539 | class FriendCate(ObjectsBase): 540 | 541 | """ 好友分类 542 | 543 | :param index: 索引 544 | :param sort: 排序 545 | :param name: 名称 546 | """ 547 | def __repr__(self): 548 | return u"".format(self.name) 549 | 550 | 551 | class Friends(ObjectsBase): 552 | 553 | def __init__(self, data): 554 | self._uin_map = {} 555 | self._name_map = {} 556 | self._mark_uin_map = {} 557 | self.update(data) 558 | 559 | def update(self, data): 560 | for item in data.get("info", {}): 561 | uin = item["uin"] 562 | if uin not in self._uin_map: 563 | info = FriendInfo(**item) 564 | self._uin_map[info.uin] = info 565 | self._name_map[info.nick] = info.uin 566 | else: 567 | self._uin_map[uin].update(**item) 568 | 569 | for item in data.get("friends", []): 570 | uin = item.get("uin") 571 | self._uin_map[uin].set_categories(item.get("categories")) 572 | 573 | for item in data.get("marknames", []): 574 | uin = item.get("uin") 575 | self._uin_map[uin].set_markname(item.get("markname")) 576 | self._mark_uin_map[item.get("markname")] = uin 577 | 578 | for item in data.get("vipinfo", []): 579 | uin = item.get("u") 580 | self._uin_map[uin].set_vipinfo(item) 581 | self.categories = [FriendCate(**kw) 582 | for kw in data.get("categories", [])] 583 | self.vipinfo = [VipInfo(**kw) for kw in data.get("vipinfo", [])] 584 | 585 | def get_uin(self, name): 586 | return self._name_map.get(name) 587 | 588 | @property 589 | def info(self): 590 | return [self._uin_map[uin] for uin in self._uin_map] 591 | 592 | def __repr__(self): 593 | return u"<{0} Friends>".format(len(self.info)) 594 | 595 | def get_nick(self, uin): 596 | """ 获取好友信息昵称 597 | """ 598 | item = self._uin_map.get(uin) 599 | if item: 600 | return item.nick 601 | 602 | def get_markname(self, uin): 603 | """ 获取好友备注信息 604 | """ 605 | item = self._uin_map.get(uin) 606 | if item: 607 | return item.markname 608 | 609 | def get_show_name(self, uin): 610 | m = self.get_markname(uin) 611 | if not m: 612 | m = self.get_nick(uin) 613 | return m 614 | 615 | def get_uin_from_mark(self, mark): 616 | return self._mark_uin_map.get(mark) 617 | 618 | def set_status(self, uin, status, client_type): 619 | item = self._uin_map.get(uin) 620 | if item: 621 | item.status = status 622 | item.client_type = client_type 623 | 624 | def set_account(self, uin, account): 625 | item = self._uin_map.get(uin) 626 | if item: 627 | item.account = account 628 | 629 | def get_account(self, uin): 630 | item = self._uin_map.get(uin) 631 | if item: 632 | return item.account 633 | -------------------------------------------------------------------------------- /plugins/_fetchtitle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf8 -*- 3 | # Author: lilydjwg (https://github.com/lilydjwg) 4 | # Source: https://github.com/lilydjwg/winterpy/blob/master/pylib/mytornado/fetchtitle.py 5 | # 由 cold (https://github.com/coldnight) 添加 Python2 支持 6 | # vim:fileencoding=utf-8 7 | 8 | import re 9 | import socket 10 | try: 11 | from urllib.parse import urlsplit, urljoin 12 | py3 = True 13 | except ImportError: 14 | from urlparse import urlsplit, urljoin # py2 15 | py3 = False 16 | 17 | from functools import partial 18 | from collections import namedtuple 19 | import struct 20 | import json 21 | import logging 22 | import encodings.idna 23 | try: 24 | # Python 3.3 25 | from html.entities import html5 as _entities 26 | def _extract_entity_name(m): 27 | return m.group()[1:] 28 | except ImportError: 29 | try: 30 | from html.entities import entitydefs as _entities 31 | except ImportError: 32 | from htmlentitydefs import entitydefs as _entities # py2 33 | 34 | def _extract_entity_name(m): 35 | return m.group()[1:-1] 36 | 37 | import tornado.ioloop 38 | import tornado.iostream 39 | 40 | # try to import C parser then fallback in pure python parser. 41 | try: 42 | from http_parser.parser import HttpParser 43 | except ImportError: 44 | try: 45 | from http_parser.pyparser import HttpParser 46 | except ImportError: 47 | from HTMLParser import HttpParser # py2 48 | 49 | UserAgent = 'FetchTitle/1.2 (wh_linux@126.com)' 50 | class SingletonFactory: 51 | def __init__(self, name): 52 | self.name = name 53 | def __repr__(self): 54 | return '<%s>' % self.name 55 | 56 | MediaType = namedtuple('MediaType', 'type size dimension') 57 | defaultMediaType = MediaType('application/octet-stream', None, None) 58 | 59 | ConnectionClosed = SingletonFactory('ConnectionClosed') 60 | TooManyRedirection = SingletonFactory('TooManyRedirection') 61 | Timeout = SingletonFactory('Timeout') 62 | 63 | logger = logging.getLogger(__name__) 64 | 65 | def _sharp2uni(code): 66 | '''&#...; ==> unicode''' 67 | s = code[1:].rstrip(';') 68 | if s.startswith('x'): 69 | return chr(int('0'+s, 16)) 70 | else: 71 | return chr(int(s)) 72 | 73 | def _mapEntity(m): 74 | name = _extract_entity_name(m) 75 | if name.startswith('#'): 76 | return _sharp2uni(name) 77 | try: 78 | return _entities[name] 79 | except KeyError: 80 | return '&' + name 81 | 82 | def replaceEntities(s): 83 | return re.sub(r'&[^;]+;', _mapEntity, s) 84 | 85 | class ContentFinder: 86 | buf = b'' 87 | def __init__(self, mediatype): 88 | self._mt = mediatype 89 | 90 | @classmethod 91 | def match_type(cls, mediatype): 92 | ctype = mediatype.type.split(';', 1)[0] 93 | if hasattr(cls, '_mime') and cls._mime == ctype: 94 | return cls(mediatype) 95 | if hasattr(cls, '_match_type') and cls._match_type(ctype): 96 | return cls(mediatype) 97 | return False 98 | 99 | class TitleFinder(ContentFinder): 100 | found = False 101 | title_begin = re.compile(b']*>', re.IGNORECASE) 102 | title_end = re.compile(b'', re.IGNORECASE) 103 | pos = 0 104 | 105 | default_charset = 'UTF-8' 106 | meta_charset = re.compile(br']+)"?\s*/?>|/"]+)"?\s*/?>', re.IGNORECASE) 107 | charset = None 108 | 109 | @staticmethod 110 | def _match_type(ctype): 111 | return ctype.find('html') != -1 112 | 113 | def __init__(self, mediatype): 114 | ctype = mediatype.type 115 | pos = ctype.find('charset=') 116 | if pos > 0: 117 | self.charset = ctype[pos+8:] 118 | if self.charset.lower() == 'gb2312': 119 | # Windows misleadingly uses gb2312 when it's gbk or gb18030 120 | self.charset = 'gb18030' 121 | 122 | def __call__(self, data): 123 | if data is not None: 124 | self.buf += data 125 | self.pos += len(data) 126 | if len(self.buf) < 100: 127 | return 128 | 129 | buf = self.buf 130 | 131 | if self.charset is None: 132 | m = self.meta_charset.search(buf) 133 | if m: 134 | self.charset = (m.group(1) or m.group(2)).decode('latin1') 135 | 136 | if not self.found: 137 | m = self.title_begin.search(buf) 138 | if m: 139 | buf = self.buf = buf[m.end():] 140 | self.found = True 141 | 142 | if self.found: 143 | m = self.title_end.search(buf) 144 | if m: 145 | raw_title = buf[:m.start()].strip() 146 | logger.debug('title found at %d', self.pos - len(buf) + m.start()) 147 | elif len(buf) > 200: # when title goes too long 148 | raw_title = buf[:200] + b'...' 149 | logger.warn('title too long, starting at %d', self.pos - len(buf)) 150 | else: 151 | raw_title = False 152 | 153 | if raw_title: 154 | return self.decode_title(raw_title) 155 | 156 | if not self.found: 157 | self.buf = buf[-100:] 158 | 159 | def decode_title(self, raw_title): 160 | try: 161 | title = replaceEntities(raw_title.decode(self.get_charset(), errors='replace')) 162 | return title 163 | except (UnicodeDecodeError, LookupError): 164 | return raw_title 165 | 166 | def get_charset(self): 167 | return self.charset or self.default_charset 168 | 169 | class PNGFinder(ContentFinder): 170 | _mime = 'image/png' 171 | def __call__(self, data): 172 | if data is None: 173 | return self._mt 174 | 175 | self.buf += data 176 | if len(self.buf) < 24: 177 | # can't decide yet 178 | return 179 | if self.buf[:16] != b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR': 180 | logging.warn('Bad PNG signature and header: %r', self.buf[:16]) 181 | return self._mt._replace(dimension='Bad PNG') 182 | else: 183 | s = struct.unpack('!II', self.buf[16:24]) 184 | return self._mt._replace(dimension=s) 185 | 186 | class JPEGFinder(ContentFinder): 187 | _mime = 'image/jpeg' 188 | isfirst = True 189 | def __call__(self, data): 190 | if data is None: 191 | return self._mt 192 | 193 | # http://www.64lines.com/jpeg-width-height 194 | if data: 195 | self.buf += data 196 | 197 | if self.isfirst is True: 198 | # finding header 199 | if len(self.buf) < 5: 200 | return 201 | if self.buf[:3] != b'\xff\xd8\xff': 202 | logging.warn('Bad JPEG signature: %r', self.buf[:3]) 203 | return self._mt._replace(dimension='Bad JPEG') 204 | else: 205 | if not py3: 206 | self.blocklen = ord(self.buf[4]) * 256 + ord(self.buf[5]) + 2 207 | else: 208 | self.blocklen = self.buf[4] * 256 + self.buf[5] + 2 209 | 210 | self.buf = self.buf[2:] 211 | self.isfirst = False 212 | 213 | if self.isfirst is False: 214 | # receiving a block. 4 is for next block size 215 | if len(self.buf) < self.blocklen + 4: 216 | return 217 | buf = self.buf 218 | if ord(buf[0]) != 0xff: 219 | logging.warn('Bad JPEG: %r', self.buf[:self.blocklen]) 220 | return self._mt._replace(dimension='Bad JPEG') 221 | if (py3 and buf[1] == 0xc0 or buf[1] == 0xc2) or\ 222 | (ord(buf[1]) == 0xc0 or (buf[1]) == 0xc2): 223 | if not py3: 224 | s = ord(buf[7]) * 256 + ord(buf[8]), ord(buf[5]) * 256 + ord(buf[6]) 225 | else: 226 | s = buf[7] * 256 + buf[8], buf[5] * 256 + buf[6] 227 | return self._mt._replace(dimension=s) 228 | else: 229 | # not Start Of Frame, retry with next block 230 | self.buf = buf = buf[self.blocklen:] 231 | if not py3: 232 | self.blocklen = ord(buf[2]) * 256 + ord(buf[3]) + 2 233 | else: 234 | self.blocklen = buf[2] * 256 + buf[3] + 2 235 | return self(b'') 236 | 237 | class GIFFinder(ContentFinder): 238 | _mime = 'image/gif' 239 | def __call__(self, data): 240 | if data is None: 241 | return self._mt 242 | 243 | self.buf += data 244 | if len(self.buf) < 10: 245 | # can't decide yet 246 | return 247 | if self.buf[:3] != b'GIF': 248 | logging.warn('Bad GIF signature: %r', self.buf[:3]) 249 | return self._mt._replace(dimension='Bad GIF') 250 | else: 251 | s = struct.unpack(' in host preparation 282 | ''' 283 | self._callback = callback 284 | self.referrer = referrer 285 | if max_follows is not None: 286 | self.max_follows = max_follows 287 | 288 | if timeout is not None: 289 | self.timeout = timeout 290 | if hasattr(tornado.ioloop, 'current'): 291 | default_io_loop = tornado.ioloop.IOLoop.current 292 | else: 293 | default_io_loop = tornado.ioloop.IOLoop.instance 294 | self.io_loop = io_loop or default_io_loop() 295 | 296 | if content_finders is not None: 297 | self._content_finders = content_finders 298 | if url_finders is not None: 299 | self._url_finders = url_finders 300 | 301 | self.origurl = url 302 | self.url_visited = [] 303 | if run_at_init: 304 | self.run() 305 | 306 | def run(self): 307 | if self.url_visited: 308 | raise Exception("can't run again") 309 | else: 310 | self.start_time = self.io_loop.time() 311 | self._timeout = self.io_loop.add_timeout( 312 | self.timeout + self.start_time, 313 | self.on_timeout, 314 | ) 315 | try: 316 | self.new_url(self.origurl) 317 | finally: 318 | self.io_loop.remove_timeout(self._timeout) 319 | 320 | def on_timeout(self): 321 | self.run_callback(Timeout) 322 | 323 | def parse_url(self, url): 324 | '''parse `url`, set self.host and return address and stream class''' 325 | self.url = u = urlsplit(url) 326 | self.host = u.netloc 327 | 328 | if u.scheme == 'http': 329 | addr = u.hostname, u.port or 80 330 | stream = tornado.iostream.IOStream 331 | elif u.scheme == 'https': 332 | addr = u.hostname, u.port or 443 333 | stream = tornado.iostream.SSLIOStream 334 | else: 335 | raise ValueError('bad url: %r' % url) 336 | 337 | return addr, stream 338 | 339 | def new_connection(self, addr, StreamClass): 340 | '''set self.addr, self.stream and connect to host''' 341 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 342 | self.addr = addr 343 | self.stream = StreamClass(s) 344 | logger.debug('%s: connecting to %s...', self.origurl, addr) 345 | self.stream.set_close_callback(self.before_connected) 346 | self.stream.connect(addr, self.send_request) 347 | 348 | def new_url(self, url): 349 | self.url_visited.append(url) 350 | self.fullurl = url 351 | 352 | for finder in self._url_finders: 353 | f = finder.match_url(url, self) 354 | if f: 355 | self.finder = f 356 | f() 357 | return 358 | 359 | addr, StreamClass = self.parse_url(url) 360 | if addr != self.addr: 361 | if self.stream: 362 | self.stream.close() 363 | self.new_connection(addr, StreamClass) 364 | else: 365 | logger.debug('%s: try to reuse existing connection to %s', self.origurl, self.addr) 366 | try: 367 | self.send_request(nocallback=True) 368 | except tornado.iostream.StreamClosedError: 369 | logger.debug('%s: server at %s doesn\'t like keep-alive, will reconnect.', self.origurl, self.addr) 370 | # The close callback should have already run 371 | self.stream.close() 372 | self.new_connection(addr, StreamClass) 373 | 374 | def run_callback(self, arg): 375 | self.io_loop.remove_timeout(self._timeout) 376 | self._finished = True 377 | if self.stream: 378 | self.stream.close() 379 | self._callback(arg, self) 380 | 381 | def send_request(self, nocallback=False): 382 | self._connected = True 383 | req = ['GET %s HTTP/1.1', 384 | 'Host: %s', 385 | # t.co will return 200 and use js/meta to redirect using the following :-( 386 | # 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:16.0) Gecko/20100101 Firefox/16.0', 387 | 'User-Agent: %s' % UserAgent, 388 | 'Accept: text/html,application/xhtml+xml;q=0.9,*/*;q=0.7', 389 | 'Accept-Language: zh-cn,zh;q=0.7,en;q=0.3', 390 | 'Accept-Charset: utf-8,gb18030;q=0.7,*;q=0.7', 391 | 'Accept-Encoding: gzip, deflate', 392 | 'Connection: keep-alive', 393 | ] 394 | if self.referrer is not None: 395 | req.append('Referer: ' + self.referrer.replace('%', '%%')) 396 | path = self.url.path or '/' 397 | if self.url.query: 398 | path += '?' + self.url.query 399 | req = '\r\n'.join(req) % ( 400 | path, self._prepare_host(self.host), 401 | ) 402 | if self._cookie: 403 | req += '\r\n' + self._cookie 404 | req += '\r\n\r\n' 405 | self.stream.write(req.encode()) 406 | self.headers_done = False 407 | self.parser = HttpParser(decompress=True) 408 | if not nocallback: 409 | self.stream.read_until_close( 410 | # self.addr and self.stream may have been changed when close callback is run 411 | partial(self.on_data, close=True, addr=self.addr, stream=self.stream), 412 | streaming_callback=self.on_data, 413 | ) 414 | 415 | def _prepare_host(self, host): 416 | if not py3: 417 | host = host.decode("utf-8") 418 | host = encodings.idna.nameprep(host) 419 | return b'.'.join(encodings.idna.ToASCII(x) for x in host.split('.')).decode('ascii') 420 | 421 | def on_data(self, data, close=False, addr=None, stream=None): 422 | if close: 423 | logger.debug('%s: connection to %s closed.', self.origurl, addr) 424 | 425 | if (close and stream and self._redirected_stream is stream) or self._finished: 426 | # The connection is closing, and we are being redirected or we're done. 427 | self._redirected_stream = None 428 | return 429 | 430 | recved = len(data) 431 | logger.debug('%s: received data: %d bytes', self.origurl, recved) 432 | 433 | p = self.parser 434 | nparsed = p.execute(data, recved) 435 | if close: 436 | # feed EOF 437 | p.execute(b'', 0) 438 | 439 | if not self.headers_done and p.is_headers_complete(): 440 | if not self.on_headers_done(): 441 | return 442 | 443 | if p.is_partial_body(): 444 | chunk = p.recv_body() 445 | if self.finder is None: 446 | # redirected but has body received 447 | return 448 | t = self.feed_finder(chunk) 449 | if t is not None: 450 | self.run_callback(t) 451 | return 452 | 453 | if p.is_message_complete(): 454 | if self.finder is None: 455 | # redirected but has body received 456 | return 457 | t = self.feed_finder(None) 458 | # if title not found, t is None 459 | self.run_callback(t) 460 | elif close: 461 | self.run_callback(self.stream.error or ConnectionClosed) 462 | 463 | def before_connected(self): 464 | '''check if something wrong before connected''' 465 | if not self._connected and not self._finished: 466 | self.run_callback(self.stream.error) 467 | 468 | def process_cookie(self): 469 | setcookie = self.headers.get('Set-Cookie', None) 470 | if not setcookie: 471 | return 472 | 473 | cookies = [c.rsplit(None, 1)[-1] for c in setcookie.split('; expires')[:-1]] 474 | self._cookie = 'Cookie: ' + '; '.join(cookies) 475 | 476 | def on_headers_done(self): 477 | '''returns True if should proceed, None if should stop for current chunk''' 478 | self.headers_done = True 479 | self.headers = self.parser.get_headers() 480 | 481 | self.status_code = self.parser.get_status_code() 482 | if self.status_code in (301, 302): 483 | self.process_cookie() # or we may be redirecting to a loop 484 | logger.debug('%s: redirect to %s', self.origurl, self.headers['Location']) 485 | self.followed_times += 1 486 | if self.followed_times > self.max_follows: 487 | self.run_callback(TooManyRedirection) 488 | else: 489 | newurl = urljoin(self.fullurl, self.headers['Location']) 490 | self._redirected_stream = self.stream 491 | self.new_url(newurl) 492 | return 493 | 494 | try: 495 | l = int(self.headers.get('Content-Length', None)) 496 | except (ValueError, TypeError): 497 | l = None 498 | 499 | ctype = self.headers.get('Content-Type', 'text/html') 500 | mt = defaultMediaType._replace(type=ctype, size=l) 501 | for finder in self._content_finders: 502 | f = finder.match_type(mt) 503 | if f: 504 | self.finder = f 505 | break 506 | else: 507 | self.run_callback(mt) 508 | return 509 | 510 | return True 511 | 512 | def feed_finder(self, chunk): 513 | '''feed data to TitleFinder, return the title if found''' 514 | t = self.finder(chunk) 515 | if t is not None: 516 | return t 517 | 518 | class URLFinder: 519 | def __init__(self, url, fetcher, match=None): 520 | self.fullurl = url 521 | self.match = match 522 | self.fetcher = fetcher 523 | 524 | @classmethod 525 | def match_url(cls, url, fetcher): 526 | if hasattr(cls, '_url_pat'): 527 | m = cls._url_pat.match(url) 528 | if m is not None: 529 | return cls(url, fetcher, m) 530 | if hasattr(cls, '_match_url') and cls._match_url(url, fetcher): 531 | return cls(url, fetcher) 532 | 533 | def done(self, info): 534 | self.fetcher.run_callback(info) 535 | 536 | class GithubFinder(URLFinder): 537 | _url_pat = re.compile(r'https://github\.com/(?!blog/)(?P[^/]+/[^/]+)/?$') 538 | _api_pat = 'https://api.github.com/repos/{repo_path}' 539 | httpclient = None 540 | 541 | def __call__(self): 542 | if self.httpclient is None: 543 | from tornado.httpclient import AsyncHTTPClient 544 | httpclient = AsyncHTTPClient() 545 | else: 546 | httpclient = self.httpclient 547 | 548 | m = self.match 549 | httpclient.fetch(self._api_pat.format(**m.groupdict()), self.parse_info, 550 | headers={ 551 | 'User-Agent': UserAgent, 552 | }) 553 | 554 | def parse_info(self, res): 555 | repoinfo = json.loads(res.body.decode('utf-8')) 556 | self.response = res 557 | self.done(repoinfo) 558 | 559 | class GithubUserFinder(GithubFinder): 560 | _url_pat = re.compile(r'https://github\.com/(?!blog(?:$|/))(?P[^/]+)/?$') 561 | _api_pat = 'https://api.github.com/users/{user}' 562 | 563 | def main(urls): 564 | class BatchFetcher: 565 | n = 0 566 | def __call__(self, title, fetcher): 567 | if isinstance(title, bytes): 568 | try: 569 | title = title.decode('gb18030') 570 | except UnicodeDecodeError: 571 | pass 572 | url = ' <- '.join(reversed(fetcher.url_visited)) 573 | logger.info('done: [%d] %s <- %s' % (fetcher.status_code, title, url)) 574 | self.n -= 1 575 | if not self.n: 576 | tornado.ioloop.IOLoop.instance().stop() 577 | 578 | def add(self, url): 579 | TitleFetcher(url, self, url_finders=(GithubFinder,)) 580 | self.n += 1 581 | 582 | from tornado.log import enable_pretty_logging 583 | enable_pretty_logging() 584 | f = BatchFetcher() 585 | for u in urls: 586 | f.add(u) 587 | tornado.ioloop.IOLoop.instance().start() 588 | 589 | def test(): 590 | urls = ( 591 | 'http://lilydjwg.is-programmer.com/', 592 | 'http://www.baidu.com', 593 | 'https://zh.wikipedia.org', # redirection 594 | 'http://redis.io/', 595 | 'http://kernel.org', 596 | 'http://lilydjwg.is-programmer.com/2012/10/27/streaming-gzip-decompression-in-python.36130.html', # maybe timeout 597 | 'http://img.vim-cn.com/22/cd42b4c776c588b6e69051a22e42dabf28f436', # image with length 598 | 'https://github.com/m13253/titlebot/blob/master/titlebot.py_', # 404 599 | 'http://lilydjwg.is-programmer.com/admin', # redirection 600 | 'http://twitter.com', # timeout 601 | 'http://www.wordpress.com', # reset 602 | 'https://www.wordpress.com', # timeout 603 | 'http://jquery-api-zh-cn.googlecode.com/svn/trunk/xml/jqueryapi.xml', # xml 604 | 'http://lilydjwg.is-programmer.com/user_files/lilydjwg/config/avatar.png', # PNG 605 | 'http://img01.taobaocdn.com/bao/uploaded/i1/110928240/T2okG7XaRbXXXXXXXX_!!110928240.jpg', # JPEG with Start Of Frame as the second block 606 | 'http://file3.u148.net/2013/1/images/1357536246993.jpg', # JPEG that failed previous code 607 | 'http://gouwu.hao123.com/', # HTML5 GBK encoding 608 | 'https://github.com/lilydjwg/winterpy', # github url finder 609 | 'http://github.com/lilydjwg/winterpy', # github url finder with redirect 610 | 'http://导航.中国/', # Punycode. This should not be redirected 611 | 'http://t.cn/zTOgr1n', # multiple redirections 612 | 'http://www.galago-project.org/specs/notification/0.9/x408.html', # 613 | 'http://x.co/dreamz', # redirection caused false ConnectionClosed error 614 | 'http://m8y.org/tmp/zipbomb/zipbomb_light_nonzero.html', # very long title 615 | ) 616 | main(urls) 617 | 618 | if __name__ == "__main__": 619 | import sys 620 | try: 621 | if len(sys.argv) == 1: 622 | sys.exit('no urls given.') 623 | elif sys.argv[1] == 'test': 624 | test() 625 | else: 626 | main(sys.argv[1:]) 627 | except KeyboardInterrupt: 628 | print('Interrupted.') 629 | -------------------------------------------------------------------------------- /twqq/hub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 13/11/14 13:14:54 7 | # Desc : 请求中间件 8 | # 9 | from __future__ import absolute_import 10 | import re 11 | import os 12 | import time 13 | import copy 14 | import json 15 | import random 16 | 17 | try: 18 | from urllib import urlencode 19 | import urllib 20 | except ImportError: 21 | import urllib.parse as urllib # py3 22 | 23 | import logging 24 | import tempfile 25 | import threading 26 | 27 | 28 | from hashlib import md5 29 | 30 | try: 31 | from cStringIO import StringIO 32 | except ImportError: 33 | from io import StringIO 34 | 35 | import pycurl 36 | 37 | from tornado.stack_context import ExceptionStackContext 38 | from tornadohttpclient import TornadoHTTPClient 39 | 40 | from requests import check_request, AcceptVerifyRequest 41 | from requests import WebQQRequest, PollMessageRequest, HeartbeatRequest 42 | from requests import SessMsgRequest, BuddyMsgRequest, GroupMsgRequest 43 | from requests import FirstRequest, Login2Request, DiscuMsgRequest 44 | from requests import FileRequest, LogoutRequset, FriendListRequest 45 | from requests import GroupMembersRequest 46 | 47 | import _hash 48 | import const 49 | import objects 50 | 51 | logger = logging.getLogger("twqq") 52 | 53 | 54 | class RequestHub(object): 55 | 56 | """ 集成Request请求和保存请求值 57 | :param qid: qq号 58 | :param pwd: 密码 59 | :param client: ~twqq.client.Client instance 60 | """ 61 | SIG_RE = re.compile(r'var g_login_sig=encodeURIComponent\("(.*?)"\);') 62 | 63 | def __init__(self, qid, pwd, client=None, debug=False, 64 | handle_msg_image=True): 65 | self.handle_msg_image = handle_msg_image 66 | self.http = TornadoHTTPClient() 67 | self.http.set_user_agent( 68 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" 69 | " (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 " 70 | "Chrome/28.0.1500.71 Safari/537.36") 71 | self.http.validate_cert = False 72 | self.http.set_global_headers({"Accept-Charset": "UTF-8,*;q=0.5"}) 73 | self.http.debug = debug 74 | 75 | self.qid = qid 76 | self.__pwd = pwd 77 | self.client = client 78 | 79 | self.rc = random.randrange(0, 100) 80 | self.aid = 1003903 # aid 固定 81 | self.clientid = random.randrange(11111111, 99999999) # 客户端id 随机固定 82 | self.msg_id = random.randrange(11111111, 99999999) # 消息id, 随机初始化 83 | self.daid = 164 84 | self.login_sig = None 85 | self.ptwebqq = None 86 | self.nickname = u"YouWillNeverGetIt" 87 | self.vfwebqq = None 88 | self.psessionid = None 89 | self.stop_poll = False 90 | 91 | # 检查是否验证码的回调 92 | self.ptui_checkVC = lambda *r: r 93 | 94 | # 是否需要验证码 95 | self.require_check = None 96 | self.require_check_time = None 97 | 98 | # 是否开始心跳和拉取消息 99 | self.poll_and_heart = None 100 | self.login_time = None 101 | self.hThread = None 102 | 103 | # 验证图片 104 | self.checkimg_path = tempfile.mktemp(".jpg") 105 | self._lock_path = tempfile.mktemp() 106 | self._wait_path = tempfile.mktemp() 107 | 108 | self.group_sig = {} # 组签名映射, 用作发送临时消息(sess_message) 109 | 110 | self.message_interval = 0.5 # 消息间隔 111 | self.last_msg_time = time.time() 112 | self.last_msg_content = None 113 | self.last_msg_numbers = 0 # 剩余位发送的消息数量 114 | WebQQRequest.hub = self 115 | self.connecting = False 116 | 117 | def connect(self): 118 | self.connecting = True 119 | self.load_next_request(FirstRequest()) 120 | 121 | def load_next_request(self, request): 122 | """ 加载下一个请求 123 | 124 | :param request: ~twqqrequests.WebQQRequest instance 125 | :rtype: ~twqqrequests.WebQQRequest instance 126 | """ 127 | func = self.http.get if request.method == WebQQRequest.METHOD_GET \ 128 | else self.http.post 129 | 130 | if self.stop_poll and isinstance(request, PollMessageRequest): 131 | logger.info("检测Poll已停止, 此请求不处理: {0}".format(request)) 132 | return 133 | 134 | kwargs = copy.deepcopy(request.kwargs) 135 | callback = request.callback if hasattr(request, "callback") and\ 136 | callable(request.callback) else None 137 | kwargs.update(callback=self.wrap(request, callback)) 138 | kwargs.update(headers=request.headers) 139 | kwargs.update(delay=request.delay) 140 | logger.debug("KWARGS: {0}".format(kwargs)) 141 | 142 | if request.ready: 143 | logger.debug("处理请求: {0}".format(request)) 144 | with ExceptionStackContext(request.handle_exc): 145 | func(request.url, request.params, **kwargs) 146 | else: 147 | logger.debug("请求未就绪: {0}".format(request)) 148 | 149 | return request 150 | 151 | def handle_pwd(self, r, vcode, huin): 152 | """ 根据检查返回结果,调用回调生成密码和保存验证码 """ 153 | if not isinstance(vcode, bytes): 154 | vcode = vcode.encode('utf-8') 155 | 156 | if not isinstance(self.__pwd, bytes): 157 | self.__pwd = self.__pwd.encode("utf-8") 158 | 159 | if not isinstance(huin, bytes): 160 | huin = huin.encode('utf-8') 161 | 162 | pwd = md5(md5(self.__pwd).digest() + huin).hexdigest().upper() 163 | pwd = md5(pwd.encode('utf-8') + vcode).hexdigest().upper() 164 | return pwd 165 | 166 | def upload_file(self, path): 167 | """ 上传文件 168 | 169 | :param path: 文件路径 170 | """ 171 | img_host = "http://dimg.vim-cn.com/" 172 | curl, buff = self.generate_curl(img_host) 173 | curl.setopt(pycurl.POST, 1) 174 | curl.setopt(pycurl.HTTPPOST, [('name', (pycurl.FORM_FILE, path)), ]) 175 | try: 176 | curl.perform() 177 | ret = buff.getvalue() 178 | curl.close() 179 | buff.close() 180 | except: 181 | logger.warn(u"上传图片错误", exc_info=True) 182 | return u"[图片获取失败]" 183 | return ret 184 | 185 | def generate_curl(self, url=None, headers=None): 186 | """ 生成一个curl, 返回 curl 实例和用于获取结果的 buffer 187 | """ 188 | curl = pycurl.Curl() 189 | buff = StringIO() 190 | 191 | curl.setopt(pycurl.COOKIEFILE, "cookie") 192 | curl.setopt(pycurl.COOKIEJAR, "cookie_jar") 193 | curl.setopt(pycurl.SHARE, self.http._share) 194 | curl.setopt(pycurl.WRITEFUNCTION, buff.write) 195 | curl.setopt(pycurl.FOLLOWLOCATION, 1) 196 | curl.setopt(pycurl.MAXREDIRS, 5) 197 | curl.setopt(pycurl.TIMEOUT, 3) 198 | curl.setopt(pycurl.CONNECTTIMEOUT, 3) 199 | 200 | if url: 201 | curl.setopt(pycurl.URL, url) 202 | 203 | if headers: 204 | self.set_curl_headers(curl, headers) 205 | 206 | return curl, buff 207 | 208 | def set_curl_headers(self, curl, headers): 209 | """ 将一个字典设置为 curl 的头 210 | """ 211 | h = [] 212 | for key, val in headers.items(): 213 | h.append("{0}: {1}".format(key, val)) 214 | curl.setopt(pycurl.HTTPHEADER, h) 215 | 216 | def get_msg_img(self, from_uin, file_path): 217 | """ 获取聊天信息中的图片 218 | """ 219 | url = "http://d.web2.qq.com/channel/get_offpic2" 220 | params = {"clientid": self.clientid, "f_uin": from_uin, 221 | "file_path": file_path, "psessionid": self.psessionid} 222 | url = url + "?" + urllib.urlencode(params) 223 | headers = {} 224 | 225 | headers = { 226 | "User-Agent": 227 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" 228 | " (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 " 229 | "Chrome/28.0.1500.71 Safari/537.36", 230 | "Referer": "http://web2.qq.com/webqq.html"} 231 | curl, buff = self.generate_curl(url, headers) 232 | try: 233 | curl.perform() 234 | except: 235 | logger.warn(u"获取聊天图片错误", exc_info=True) 236 | return u"[图片获取失败]" 237 | body = buff.getvalue() 238 | curl.close() 239 | buff.close() 240 | 241 | path = tempfile.mktemp() 242 | with open(path, 'w') as f: 243 | f.write(body) 244 | return self.upload_file(path) 245 | 246 | def get_group_img(self, gid, from_uin, file_id, server, name, key, 247 | _type=0): 248 | """ 获取群发送的图片 249 | """ 250 | ip, port = server.split(":") 251 | url = "http://web2.qq.com/cgi-bin/get_group_pic" 252 | params = {"type": _type, "fid": file_id, "gid": gid, "pic": name, 253 | "rip": ip, "rport": port, "uin": from_uin, 254 | "vfwebqq": self.vfwebqq} 255 | url = url + "?" + urllib.urlencode(params) 256 | headers = { 257 | "User-Agent": 258 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" 259 | " (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 " 260 | "Chrome/28.0.1500.71 Safari/537.36", 261 | "Referer": "http://web2.qq.com/webqq.html"} 262 | curl, buff = self.generate_curl(url, headers) 263 | try: 264 | curl.perform() 265 | except: 266 | logger.warn(u"获取群聊天图片错误", exc_info=True) 267 | return u"[图片获取失败]" 268 | body = buff.getvalue() 269 | buff.close() 270 | curl.close() 271 | path = tempfile.mktemp() 272 | with open(path, 'w') as f: 273 | f.write(body) 274 | return self.upload_file(path) 275 | 276 | def set_friends(self, data): 277 | """ 存储好友信息 278 | """ 279 | if not hasattr(self, "_friends"): 280 | self._friends = objects.Friends(data) 281 | else: 282 | self._friends.update(data) 283 | 284 | def get_friends(self): 285 | return self._friends if hasattr(self, "_friends") else None 286 | 287 | def set_groups(self, data): 288 | if not hasattr(self, "_groups"): 289 | self._groups = objects.GroupList(data) 290 | else: 291 | self._groups.update(data) 292 | 293 | def get_groups(self): 294 | return self._groups if hasattr(self, "_groups") else None 295 | 296 | def set_discu(self, data): 297 | if not hasattr(self, "_discu"): 298 | self._discu = objects.DiscuList(data) 299 | else: 300 | self._discu.update(data) 301 | 302 | def get_discu(self): 303 | return self._discu if hasattr(self, "_discu") else None 304 | 305 | def lock(self): 306 | """ 当输入验证码时锁住 307 | """ 308 | with open(self._lock_path, 'w'): 309 | pass 310 | 311 | def get_account(self, uin, _type=1): 312 | """ 获取好友QQ号 313 | :param _type: 类型, 1 是好友和讨论组, 4 是群 314 | """ 315 | # self.load_next_request(QQNumberRequest()) 316 | ret = self.get_friends().get_account(uin) 317 | if ret: 318 | return ret 319 | 320 | url = "http://s.web2.qq.com/api/get_friend_uin2" 321 | params = {"code": "", "t": time.time() * 1000, "tuin": uin, 322 | "type": _type, "verifysession": "", "vfwebqq": self.vfwebqq} 323 | url = url + "?" + urllib.urlencode(params) 324 | headers = { 325 | "User-Agent": 326 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" 327 | " (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 " 328 | "Chrome/28.0.1500.71 Safari/537.36", 329 | "Referer": const.S_REFERER} 330 | curl, buff = self.generate_curl(url, headers) 331 | 332 | try: 333 | curl.perform() 334 | ret = buff.getvalue() 335 | buff.close() 336 | data = json.loads(ret) 337 | curl.close() 338 | except: 339 | logger.warn(u"获取QQ号时发生错误", exc_info=True) 340 | return 341 | 342 | if data.get("retcode") == 0: 343 | logger.info(u"获取QQ号码成功: {0!r}".format(data)) 344 | ret = data.get("result") 345 | uin = ret.get("uin") 346 | account = ret.get("account") 347 | self.get_friends().set_account(uin, account) 348 | return account 349 | logger.warn(u"获取QQ号码失败: {0!r}".format(data)) 350 | 351 | def unlock(self): 352 | """ 解锁 353 | """ 354 | if os.path.exists(self._lock_path): 355 | os.remove(self._lock_path) 356 | 357 | def clean(self): 358 | """ 清除锁住和等待状态 359 | """ 360 | self.unlock() 361 | self.unwait() 362 | 363 | def wait(self): 364 | """ 当没有验证是否需要验证码时等待 365 | """ 366 | with open(self._wait_path, 'w'): 367 | pass 368 | 369 | def unwait(self): 370 | """ 解除等待状态 371 | """ 372 | if os.path.exists(self._wait_path): 373 | os.remove(self._wait_path) 374 | 375 | def is_lock(self): 376 | """ 检测是否被锁住 377 | """ 378 | return os.path.exists(self._lock_path) 379 | 380 | def is_wait(self): 381 | """ 检测是否在等待生成验证码 382 | """ 383 | return os.path.exists(self._wait_path) 384 | 385 | def _hash(self): 386 | """ 获取好友列表时的Hash """ 387 | return _hash.webqq_hash(self.qid, self.ptwebqq) 388 | 389 | def start_poll(self): 390 | """ 开始心跳和拉取信息 391 | """ 392 | self.stop_poll = False 393 | if not self.poll_and_heart: 394 | self.login_time = time.time() 395 | logger.info("开始拉取信息") 396 | self.load_next_request(PollMessageRequest()) 397 | self.poll_and_heart = True 398 | if self.hThread is None: 399 | logger.info("开始心跳") 400 | self.hThread = threading.Thread(target=self._heartbeat) 401 | self.hThread.setDaemon(True) 402 | self.hThread.start() 403 | 404 | def _heartbeat(self): 405 | """ 放入线程的产生心跳 406 | """ 407 | assert not isinstance(threading.currentThread(), threading._MainThread) 408 | while 1: 409 | try: 410 | self.load_next_request(HeartbeatRequest()) 411 | except: 412 | pass 413 | time.sleep(60) 414 | 415 | def make_msg_content(self, content, style): 416 | """ 构造QQ消息的内容 417 | 418 | :param content: 小心内容 419 | :type content: str 420 | :rtype: str 421 | """ 422 | self.msg_id += 1 423 | return json.dumps([content, 424 | ["font", style]]) 425 | 426 | def get_delay(self, content): 427 | """ 根据消息内容是否和上一条内容相同和未送出的消息数目产生延迟 428 | 429 | :param content: 消息内容 430 | :rtype: tuple(delay, number) 431 | """ 432 | MIN = self.message_interval 433 | delay = 0 434 | sub = time.time() - self.last_msg_time 435 | if self.last_msg_numbers < 0: 436 | self.last_msg_numbers = 0 437 | 438 | # 不足最小间隔就补足最小间隔 439 | if sub < MIN: 440 | delay = MIN 441 | logger.debug(u"间隔 %s 小于 %s, 设置延迟为%s", sub, MIN, delay) 442 | 443 | # 如果间隔是已有消息间隔的2倍, 则清除已有消息数 444 | # print "sub", sub, "n:", self.last_msg_numbers 445 | if self.last_msg_numbers > 0 and\ 446 | sub / (MIN * self.last_msg_numbers) > 1: 447 | self.last_msg_numbers = 0 448 | 449 | # 如果还有消息未发送, 则加上他们的间隔 450 | if self.last_msg_numbers > 0: 451 | delay += MIN * self.last_msg_numbers 452 | logger.info(u"有%s条消息未发送, 延迟为 %s", 453 | self.last_msg_numbers, delay) 454 | 455 | n = 1 456 | # 如果这条消息和上条消息一致, 保险起见再加上一个最小间隔 457 | if self.last_msg_content == content and sub < MIN: 458 | delay += MIN 459 | self.last_msg_numbers += 1 460 | n = 2 461 | 462 | self.last_msg_numbers += 1 463 | self.last_msg_content = content 464 | 465 | if delay: 466 | logger.info(u"有 {1} 个消息未投递将会在 {0} 秒后投递" 467 | .format(delay, self.last_msg_numbers)) 468 | # 返回消息累加个数, 在消息发送后减去相应的数目 469 | return delay, n 470 | 471 | def consume_delay(self, number): 472 | """ 消费延迟 473 | 474 | :param number: 消费的消息数目 475 | """ 476 | self.last_msg_numbers -= number 477 | self.last_msg_time = time.time() 478 | 479 | def get_group_id(self, uin): 480 | """ 根据组uin获取组的id 481 | 482 | :param uin: 组的uin 483 | """ 484 | return self.get_groups().get_gid(uin) 485 | 486 | def get_friend_name(self, uin): 487 | """ 获取好友名称 488 | 489 | :param uin: 好友uin 490 | """ 491 | return self.get_friends().get_show_name(uin) 492 | 493 | def wrap(self, request, func=None): 494 | """ 装饰callback 495 | 496 | :param request: ~twqqrequests.WebQQRequest instance 497 | :param func: 回调函数 498 | """ 499 | def _wrap(resp, *args, **kwargs): 500 | data = resp.body 501 | logger.debug(resp.headers) 502 | if resp.headers.get("Content-Type") == "application/json": 503 | data = json.loads(data) if data else {} 504 | else: 505 | try: 506 | data = json.loads(data) 507 | except: 508 | pass 509 | if func: 510 | func(resp, data, *args, **kwargs) 511 | 512 | funcs = self.client.request_handlers.get( 513 | check_request(request), []) 514 | for f in funcs: 515 | f(request, resp, data) 516 | 517 | return _wrap 518 | 519 | def handle_qq_msg_contents(self, from_uin, contents, eid=None, _type=0): 520 | """ 处理QQ消息内容 521 | 522 | :param from_uin: 消息发送人uin 523 | :param contents: 内容 524 | :param eid: 扩展id(群gid, 讨论组did) 525 | :type contents: list 526 | 527 | """ 528 | content = "" 529 | for row in contents: 530 | if isinstance(row, (list)) and len(row) == 2: 531 | info = row[1] 532 | if row[0] == "offpic" and self.handle_msg_image: 533 | file_path = info.get("file_path") 534 | content += self.get_msg_img(from_uin, file_path) 535 | 536 | if row[0] == "cface" and self.handle_msg_image: 537 | name = info.get("name") 538 | key = info.get("key") 539 | file_id = info.get("file_id") 540 | server = info.get("server") 541 | content += self.get_group_img(eid, from_uin, file_id, 542 | server, name, key, _type) 543 | 544 | if isinstance(row, (str, unicode)): 545 | content += row.replace(u"【提示:此用户正在使用Q+" 546 | u" Web:http://web.qq.com/】", "")\ 547 | .replace(u"【提示:此用户正在使用Q+" 548 | u" Web:http://web3.qq.com/】", "") 549 | return content.replace("\r", "\n").replace("\r\n", "\n")\ 550 | .replace("\n\n", "\n") 551 | 552 | def get_group_member_nick(self, gcode, uin): 553 | """ 根据组代码和用户uin获取群成员昵称 554 | 555 | :param gcode: 组代码 556 | :param uin: 群成员uin 557 | """ 558 | return self.get_groups().get_member_nick(gcode, uin) 559 | 560 | def dispatch(self, qq_source): 561 | """ 调度QQ消息 562 | 563 | :param qq_source: 源消息包 564 | """ 565 | if self.stop_poll: 566 | logger.info("检测Poll已停止, 此消息不处理: {0}".format(qq_source)) 567 | return 568 | 569 | if qq_source.get("retcode") == 0: 570 | messages = qq_source.get("result") 571 | logger.info(u"获取消息: {0}".format(messages)) 572 | for m in messages: 573 | poll_type = m.get("poll_type") 574 | if poll_type == "buddies_status_change": 575 | self.get_friends().set_status(**m.get("value", {})) 576 | else: 577 | funcs = self.client.msg_handlers.get(m.get("poll_type"), 578 | []) 579 | [func(*func._args_func(self, m)) for func in funcs] 580 | 581 | def recv_file(self, guid, lcid, to, callback): 582 | """ 接收文件 583 | 584 | :param guid: 文件名 585 | :param lcid: 会话id 586 | :param to_uin: 发送人uin 587 | :param callback: 回调, 接收两个参数, 分别是文件名和文件内容 588 | """ 589 | self.load_next_request(FileRequest(guid, lcid, to, callback)) 590 | 591 | def relogin(self): 592 | """ 被T出或获取登出时尝试重新登录 593 | """ 594 | self.stop_poll = True 595 | self.poll_and_heart = None 596 | self.load_next_request(Login2Request(relogin=True)) 597 | 598 | def disconnect(self): 599 | self.stop_poll = True 600 | self.poll_and_heart = None 601 | self.load_next_request(LogoutRequset()) 602 | 603 | def send_sess_msg(self, qid, to_uin, content, style=const.DEFAULT_STYLE): 604 | """ 发送临时消息 605 | 606 | :param qid: 发送临时消息的qid 607 | :param to_uin: 消息接收人 608 | :param content: 消息内容 609 | :rtype: Request instance 610 | """ 611 | return self.load_next_request(SessMsgRequest(qid, to_uin, content, 612 | style)) 613 | 614 | def send_group_msg(self, group_uin, content, style=const.DEFAULT_STYLE): 615 | """ 发送群消息 616 | 617 | :param group_uin: 组的uin 618 | :param content: 消息内容 619 | :rtype: Request instance 620 | """ 621 | return self.load_next_request(GroupMsgRequest(group_uin, content, 622 | style)) 623 | 624 | def send_discu_msg(self, did, content, style=const.DEFAULT_STYLE): 625 | """ 发送讨论组消息 626 | 627 | :param did: 讨论组id 628 | :param content: 内容 629 | """ 630 | return self.load_next_request(DiscuMsgRequest(did, content, style)) 631 | 632 | def send_buddy_msg(self, to_uin, content, style=const.DEFAULT_STYLE): 633 | """ 发送好友消息 634 | 635 | :param to_uin: 消息接收人 636 | :param content: 消息内容 637 | :rtype: Request instance 638 | """ 639 | return self.load_next_request(BuddyMsgRequest(to_uin, content, style)) 640 | 641 | def send_msg_with_markname(self, markname, content): 642 | """ 使用备注名发送消息 643 | 644 | :param markname: 备注名 645 | :param content: 消息内容 646 | :rtype: None or Request instance 647 | """ 648 | uin = self.get_friends().get_uin_from_mark(markname) 649 | if not uin: 650 | return 651 | return self.send_buddy_msg(uin, content) 652 | 653 | def accept_verify(self, uin, account, markname=""): 654 | """ 同意验证请求 655 | 656 | :param uin: 请求人uin 657 | :param account: 请求人账号 658 | :param markname: 添加后的备注 659 | """ 660 | return self.load_next_request(AcceptVerifyRequest(uin, account, 661 | markname)) 662 | 663 | def refresh_friend_info(self): 664 | self.load_next_request(FriendListRequest(manual=True)) 665 | 666 | def refresh_group_info(self, _id): 667 | """ 手动刷新某个群的信息 668 | 669 | :param _id: 对应群生成的唯一id 670 | """ 671 | gcode, _type = objects.UniqueIds.get(int(_id)) 672 | if gcode is None or _type is None: 673 | return False, u"没有找到对象" 674 | 675 | if _type != objects.UniqueIds.T_GRP: 676 | return False, u"该对象不是群" 677 | 678 | self.load_next_request(GroupMembersRequest(gcode)) 679 | return True, self.get_groups().get_group_name(gcode) 680 | -------------------------------------------------------------------------------- /twqq/requests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 13/11/11 18:21:10 7 | # Desc : 8 | # 9 | import os 10 | import sys 11 | import json 12 | import time 13 | import random 14 | import inspect 15 | import logging 16 | 17 | import const 18 | 19 | logger = logging.getLogger("twqq") 20 | 21 | 22 | class WebQQRequest(object): 23 | METHOD_POST = "post" 24 | METHOD_GET = "get" 25 | 26 | hub = None 27 | url = None 28 | params = {} 29 | headers = {} 30 | method = METHOD_GET # 默认请求为GET 31 | kwargs = {} 32 | ready = True 33 | 34 | def __init__(self, *args, **kwargs): 35 | self.delay = kwargs.pop("delay", 0) 36 | self.init(*args, **kwargs) 37 | 38 | def handle_exc(self, type, value, trace): 39 | pass 40 | 41 | def handle_retcode(self, data, msg): 42 | if isinstance(data, dict): 43 | retcode = data.get("retcode") 44 | if retcode == 0: 45 | logger.info(u"{0} 成功".format(msg)) 46 | elif retcode == 8: 47 | logger.error(u"{0} 失败, 需要重新登录".format(msg)) 48 | 49 | return 50 | 51 | logger.warn(u"{0} 失败 <{1}>".format(msg, data)) 52 | 53 | 54 | class LoginSigRequest(WebQQRequest): 55 | url = "https://ui.ptlogin2.qq.com/cgi-bin/login" 56 | 57 | def init(self): 58 | self.hub.wait() 59 | logger.info("获取 login_sig...") 60 | self.params = [("daid", self.hub.daid), ("target", "self"), 61 | ("style", 5), ("mibao_css", "m_webqq"), 62 | ("appid", self.hub.aid), ("enable_qlogin", 0), 63 | ("no_verifyimg", 1), 64 | ("s_url", "http://web2.qq.com/loginproxy.html"), 65 | ("f_url", "loginerroralert"), 66 | ("strong_login", 1), ("login_state", 10), 67 | ("t", "20130723001")] 68 | 69 | def callback(self, resp, data): 70 | if not data: 71 | logger.warn(u"没有获取到 Login Sig, 重新获取") 72 | return self.hub.load_next_request(self) 73 | 74 | sigs = self.hub.SIG_RE.findall(str(resp.body)) 75 | if len(sigs) == 1: 76 | self.hub.login_sig = sigs[0] 77 | logger.info(u"获取Login Sig: {0}".format(self.hub.login_sig)) 78 | else: 79 | logger.warn(u"没有获取到 Login Sig, 后续操作可能失败") 80 | 81 | self.hub.load_next_request(CheckRequest()) 82 | 83 | 84 | class CheckRequest(WebQQRequest): 85 | 86 | """ 检查是否需要验证码 87 | """ 88 | url = "http://check.ptlogin2.qq.com/check" 89 | 90 | def init(self): 91 | self.params = {"uin": self.hub.qid, "appid": self.hub.aid, 92 | "u1": const.CHECK_U1, "login_sig": self.hub.login_sig, 93 | "js_ver": 10040, "js_type": 0, "r": random.random()} 94 | self.headers.update({"Referer": const.CHECK_REFERER}) 95 | 96 | def callback(self, resp, data): 97 | if data is None: 98 | logger.warn("检查验证码, 没有返回数据. 退出, 请尝试重启程序") 99 | sys.exit(-1) 100 | logger.info(u"检查验证码返回: {}".format(data)) 101 | r, vcode, uin = eval(b"self.hub." + data.strip().rstrip(b";"))[:3] 102 | logger.debug("R:{0} vcode:{1}".format(r, vcode)) 103 | self.hub.clean() 104 | if int(r) == 0: 105 | logger.info("验证码检查完毕, 不需要验证码") 106 | password = self.hub.handle_pwd(r, vcode, uin) 107 | self.hub.check_code = vcode 108 | self.hub.load_next_request(BeforeLoginRequest(password)) 109 | else: 110 | logger.warn("验证码检查完毕, 需要验证码") 111 | self.hub.require_check = True 112 | self.hub.load_next_request(VerifyCodeRequest(r, vcode, uin)) 113 | 114 | 115 | class VerifyCodeRequest(WebQQRequest): 116 | url = "https://ssl.captcha.qq.com/getimage" 117 | 118 | def init(self, r, vcode, uin): 119 | self.r, self.vcode, self.uin = r, vcode, uin 120 | self.params = [("aid", self.hub.aid), ("r", random.random()), 121 | ("uin", self.hub.qid)] 122 | 123 | def callback(self, resp, data): 124 | self.hub.require_check_time = time.time() 125 | with open(self.hub.checkimg_path, 'wb') as f: 126 | f.write(resp.body) 127 | self.hub.unwait() 128 | 129 | self.hub.client.handle_verify_code(self.hub.checkimg_path, self.r, 130 | self.uin) 131 | 132 | 133 | class BeforeLoginRequest(WebQQRequest): 134 | 135 | """ 登录前的准备 136 | """ 137 | url = "https://ssl.ptlogin2.qq.com/login" 138 | 139 | def init(self, password): 140 | if isinstance(self.hub.check_code, bytes): # py3 141 | vcode = self.hub.check_code.decode('utf-8') 142 | else: 143 | vcode = self.hub_check_code 144 | 145 | self.hub.unwait() 146 | self.hub.lock() 147 | self.params = [("u", self.hub.qid), ("p", password), 148 | ("verifycode", vcode), 149 | ("webqq_type", 10), ("remember_uin", 1), 150 | ("login2qq", 1), 151 | ("aid", self.hub.aid), ("u1", const.BLOGIN_U1), 152 | ("h", 1), ("action", '4-5-8246'), ("ptredirect", 0), 153 | ("ptlang", 2052), ("from_ui", 1), 154 | ("daid", self.hub.daid), 155 | ("pttype", 1), ("dumy", ""), ("fp", "loginerroralert"), 156 | ("mibao_css", "m_webqq"), ("t", 1), ("g", 1), 157 | ("js_type", 0), 158 | ("js_ver", 10040), ("login_sig", self.hub.login_sig)] 159 | referer = const.BLOGIN_R_REFERER if self.hub.require_check else\ 160 | const.BLOGIN_REFERER 161 | self.headers.update({"Referer": referer}) 162 | 163 | def ptuiCB(self, scode, r, url, status, msg, nickname=None): 164 | """ 模拟JS登录之前的回调, 保存昵称 """ 165 | return scode, r, url, status, msg, nickname 166 | 167 | def get_back_args(self, data): 168 | blogin_data = data.decode("utf-8").strip().rstrip(";") 169 | logger.info(u"登录返回数据: {0}".format(blogin_data)) 170 | return eval("self." + blogin_data) 171 | 172 | def check(self, scode, r, url, status, msg, nickname=None): 173 | self.hub.unlock() 174 | if int(scode) == 0: 175 | logger.info("从Cookie中获取ptwebqq的值") 176 | try: 177 | val = self.hub.http.cookie['.qq.com']['/']['ptwebqq'].value 178 | self.hub.ptwebqq = val 179 | except: 180 | logger.error("从Cookie中获取ptwebqq的值失败, 退出..") 181 | sys.exit(-1) 182 | elif int(scode) == 4: 183 | logger.error(msg) 184 | return False, self.hub.load_next_request(CheckRequest()) 185 | else: 186 | if isinstance(msg, bytes): 187 | msg = msg.decode('utf-8') 188 | logger.error(u"server response: {0}".format(msg)) 189 | return False, self.hub.load_next_request(CheckRequest()) 190 | 191 | if nickname: 192 | self.hub.nickname = nickname.decode('utf-8') 193 | 194 | return True, url 195 | 196 | def callback(self, resp, data): 197 | if not data: 198 | logger.error("没有数据返回, 登录失败, 尝试重新登录") 199 | return self.hub.load_next_request(CheckRequest()) 200 | 201 | args = self.get_back_args(resp.body) 202 | r, url = self.check(*args) 203 | if r: 204 | logger.info("检查完毕") 205 | self.hub.load_next_request(LoginRequest(url)) 206 | 207 | 208 | class LoginRequest(WebQQRequest): 209 | 210 | """ 登录前的准备 211 | """ 212 | 213 | def init(self, url): 214 | logger.info("开始登录前准备...") 215 | self.url = url 216 | self.headers.update(Referer=const.LOGIN_REFERER) 217 | 218 | def callback(self, resp, data): 219 | if os.path.exists(self.hub.checkimg_path): 220 | os.remove(self.hub.checkimg_path) 221 | 222 | self.hub.load_next_request(Login2Request()) 223 | 224 | 225 | class Login2Request(WebQQRequest): 226 | 227 | """ 真正的登录 228 | """ 229 | url = "http://d.web2.qq.com/channel/login2" 230 | method = WebQQRequest.METHOD_POST 231 | 232 | def init(self, relogin=False): 233 | self.relogin = relogin 234 | logger.info("准备完毕, 开始登录") 235 | self.headers.update(Referer=const.S_REFERER, Origin=const.D_ORIGIN) 236 | self.params = [("r", json.dumps({"status": "online", 237 | "ptwebqq": self.hub.ptwebqq, 238 | "passwd_sig": "", 239 | "clientid": self.hub.clientid, 240 | "psessionid": None})), 241 | ("clientid", self.hub.clientid), 242 | ("psessionid", "null")] 243 | 244 | def callback(self, resp, data): 245 | self.hub.require_check_time = None 246 | if not resp.body or not isinstance(data, dict): 247 | logger.error(u"没有获取到数据或数据格式错误, 登录失败:{0}" 248 | .format(resp.body)) 249 | self.hub.load_next_request(FirstRequest()) 250 | return 251 | 252 | if data.get("retcode") != 0: 253 | logger.error("登录失败 {0!r}".format(data)) 254 | self.hub.load_next_request(FirstRequest()) 255 | return 256 | self.hub.vfwebqq = data.get("result", {}).get("vfwebqq") 257 | self.hub.psessionid = data.get("result", {}).get("psessionid") 258 | 259 | if not self.relogin: 260 | logger.info("登录成功, 开始加载好友列表") 261 | self.hub.load_next_request(FriendListRequest()) 262 | else: 263 | logger.info("重新登录成功, 开始拉取消息") 264 | self.hub.start_poll() 265 | 266 | 267 | class FriendListRequest(WebQQRequest): 268 | 269 | """ 加载好友信息 270 | """ 271 | url = "http://s.web2.qq.com/api/get_user_friends2" 272 | method = WebQQRequest.METHOD_POST 273 | 274 | def init(self, first=True, manual=False): 275 | self.is_first = first 276 | self.manual = manual 277 | self.params = [("r", json.dumps({"h": "hello", 278 | "hash": self.hub._hash(), 279 | "vfwebqq": self.hub.vfwebqq}))] 280 | self.headers.update(Referer=const.S_REFERER) 281 | 282 | def callback(self, resp, data): 283 | if not resp.body and self.is_first: 284 | logger.error("加载好友信息失败, 重新开始登录") 285 | return self.hub.load_next_request(FirstRequest()) 286 | if data.get("retcode") != 0 and self.is_first: 287 | logger.error("加载好友信息失败, 重新开始登录") 288 | return self.hub.load_next_request(FirstRequest()) 289 | 290 | if isinstance(data, dict) and data.get("retcode") == 0: 291 | info = data.get("result", {}) 292 | self.hub.set_friends(info) 293 | friends = self.hub.get_friends() 294 | logger.info(u"加载好友信息 {0!r}".format(friends)) 295 | logger.debug(data) 296 | self.hub.load_next_request(FriendStatusRequest()) 297 | else: 298 | logger.warn(u"加载好友列表失败: {0!r}".format(data)) 299 | 300 | if not self.manual: 301 | self.hub.load_next_request(GroupListRequest()) 302 | self.hub.load_next_request(DiscuListRequest()) 303 | self.hub.load_next_request(FriendListRequest(delay=3600, 304 | first=False)) 305 | 306 | 307 | FriendInfoRequest = FriendListRequest 308 | import warnings 309 | warnings.warn("In next version we will rename twqq.requests.FreindInfoRequest " 310 | "to twqq.requests.FriendListRequest") 311 | 312 | 313 | class FriendStatusRequest(WebQQRequest): 314 | 315 | """ 获取在线好友状态 316 | """ 317 | 318 | url = "https://d.web2.qq.com/channel/get_online_buddies2" 319 | 320 | def init(self): 321 | self.params = {"clientid": self.hub.clientid, 322 | "psessionid": self.hub.psessionid, 323 | "t": int(time.time() * 1000)} 324 | self.headers.update(Referer=const.D_REFERER) 325 | 326 | def callback(self, response, data): 327 | logger.info(u"加载好友状态信息: {0!r}".format(data)) 328 | if isinstance(data, dict) and data.get("retcode") == 0: 329 | for item in data.get('result', []): 330 | self.hub.get_friends().set_status(**item) 331 | else: 332 | logger.warn(u"加载好友状态信息失败: {0!r}".format(data)) 333 | 334 | 335 | class GroupListRequest(WebQQRequest): 336 | 337 | """ 获取群列表 338 | """ 339 | url = "http://s.web2.qq.com/api/get_group_name_list_mask2" 340 | method = WebQQRequest.METHOD_POST 341 | 342 | def init(self): 343 | self.params = {"r": json.dumps({"vfwebqq": self.hub.vfwebqq, 344 | "hash": self.hub._hash(), 345 | })} 346 | self.headers.update(Origin=const.S_ORIGIN) 347 | self.headers.update(Referer=const.S_REFERER) 348 | logger.info("获取群列表") 349 | 350 | def callback(self, resp, data): 351 | logger.debug(u"群信息 {0!r}".format(data)) 352 | self.hub.set_groups(data.get("result", {})) 353 | groups = self.hub.get_groups() 354 | logger.info(u"群列表: {0!r}".format(groups)) 355 | if not groups.gnamelist: 356 | self.hub.start_poll() 357 | 358 | for i, gcode in enumerate(groups.get_gcodes()): 359 | self.hub.load_next_request(GroupMembersRequest(gcode, i == 0)) 360 | 361 | 362 | class GroupMembersRequest(WebQQRequest): 363 | 364 | """ 获取群成员 365 | 366 | :param gcode: 群代码 367 | :param poll: 是否开始拉取信息和心跳 368 | :type poll: boolean 369 | """ 370 | url = "http://s.web2.qq.com/api/get_group_info_ext2" 371 | 372 | def init(self, gcode, poll=False): 373 | self._poll = poll 374 | self._gcode = gcode 375 | self.params = [("gcode", gcode), ("vfwebqq", self.hub.vfwebqq), 376 | ("cb", "undefined"), ("t", int(time.time() * 1000))] 377 | self.headers.update(Referer=const.S_REFERER) 378 | 379 | def callback(self, resp, data): 380 | if isinstance(data, dict) and data.get("retcode") == 0: 381 | logger.debug(u"获取群成员信息 {0!r}".format(data)) 382 | members = data.get("result", {}) 383 | groups = self.hub.get_groups() 384 | group = groups.find_group(self._gcode) 385 | group.set_group_detail(members) 386 | logger.debug(u"群详细信息: {0}".format(group)) 387 | else: 388 | logger.warn(u"获取群成员信息失败 {0!r}".format(data)) 389 | 390 | if self._poll: 391 | self.hub.start_poll() 392 | 393 | 394 | class HeartbeatRequest(WebQQRequest): 395 | 396 | """ 心跳请求 397 | """ 398 | url = "http://web.qq.com/web2/get_msg_tip" 399 | kwargs = dict(request_timeout=0.5, connect_timeout=0.5) 400 | 401 | def init(self): 402 | self.params = dict([("uin", ""), ("tp", 1), ("id", 0), ("retype", 1), 403 | ("rc", self.hub.rc), ("lv", 3), 404 | ("t", int(time.time() * 1000))]) 405 | self.hub.rc += 1 406 | 407 | def callback(self, resp, data): 408 | logger.info("心跳...") 409 | 410 | 411 | class PollMessageRequest(WebQQRequest): 412 | 413 | """ 拉取消息请求 414 | """ 415 | url = "http://d.web2.qq.com/channel/poll2" 416 | method = WebQQRequest.METHOD_POST 417 | kwargs = {"request_timeout": 60.0, "connect_timeout": 60.0} 418 | 419 | def init(self): 420 | rdic = {"clientid": self.hub.clientid, 421 | "psessionid": self.hub.psessionid, "key": 0, "ids": []} 422 | self.params = [("r", json.dumps(rdic)), 423 | ("clientid", self.hub.clientid), 424 | ("psessionid", self.hub.psessionid)] 425 | self.headers.update(Referer=const.D_REFERER) 426 | self.headers.update(Origin=const.D_ORIGIN) 427 | self.ready = not self.hub.stop_poll 428 | 429 | def callback(self, resp, data): 430 | polled = False 431 | try: 432 | if not data: 433 | return 434 | 435 | if data.get("retcode") in [121, 120]: 436 | logger.error("获取登出消息, 尝试重新登录") 437 | self.hub.relogin() 438 | return 439 | 440 | logger.info(u"获取消息: {0!r}".format(data)) 441 | self.hub.load_next_request(PollMessageRequest()) 442 | polled = True 443 | self.hub.dispatch(data) 444 | except Exception as e: 445 | logger.error(u"消息获取异常: {0}".format(e), exc_info=True) 446 | finally: 447 | if not polled: 448 | self.hub.load_next_request(PollMessageRequest()) 449 | 450 | 451 | class SessGroupSigRequest(WebQQRequest): 452 | 453 | """ 获取临时消息群签名请求 454 | 455 | :param qid: 临时签名对应的qid(对应群的gid) 456 | :param to_uin: 临时消息接收人uin 457 | :param sess_reqeust: 发起临时消息的请求 458 | """ 459 | 460 | url = "https://d.web2.qq.com/channel/get_c2cmsg_sig2" 461 | 462 | def init(self, qid, to_uin, sess_reqeust): 463 | self.sess_request = sess_reqeust 464 | self.to_uin = to_uin 465 | self.params = (("id", qid), ("to_uin", to_uin), 466 | ("service_type", 0), ("clientid", self.hub.clientid), 467 | ("psessionid", self.hub.psessionid), ("t", time.time())) 468 | self.headers.update(Referer=const.S_REFERER) 469 | 470 | def callback(self, resp, data): 471 | result = data.get("result") 472 | group_sig = result.get("value") 473 | if data.get("retcode") != 0: 474 | logger.warn(u"加载临时消息签名失败: {0}".format(group_sig)) 475 | return 476 | 477 | logger.info(u"加载临时消息签名 {0} for {1}".format(group_sig, self.to_uin)) 478 | self.hub.group_sig[self.to_uin] = group_sig 479 | self.sess_request.ready = True 480 | self.sess_request.init_params(group_sig) 481 | self.hub.load_next_request(self.sess_request) 482 | 483 | 484 | class SessMsgRequest(WebQQRequest): 485 | 486 | """ 发送临时消息请求 487 | 488 | :param qid: 临时消息qid 489 | :param to_uin: 接收人 uin 490 | :param content: 发送内容 491 | """ 492 | url = "https://d.web2.qq.com/channel/send_sess_msg2" 493 | method = WebQQRequest.METHOD_POST 494 | 495 | def init(self, qid, to_uin, content, style): 496 | self.to = to_uin 497 | self._content = content 498 | self.content = self.hub.make_msg_content(content, style) 499 | group_sig = self.hub.group_sig.get(to_uin) 500 | if not group_sig: 501 | self.ready = False 502 | self.hub.load_next_request(SessGroupSigRequest(qid, to_uin, self)) 503 | else: 504 | self.init_params(group_sig) 505 | 506 | def init_params(self, group_sig): 507 | self.delay, self.number = self.hub.get_delay(self._content) 508 | self.params = (("r", json.dumps({"to": self.to, "group_sig": group_sig, 509 | "face": 549, "content": self.content, 510 | "msg_id": self.hub.msg_id, 511 | "service_type": 0, 512 | "clientid": self.hub.clientid, 513 | "psessionid": self.hub.psessionid})), 514 | ("clientid", self.hub.clientid), 515 | ("psessionid", self.hub.psessionid)) 516 | 517 | def callback(self, resp, data): 518 | self.handle_retcode(data, u"[临时消息] {0} ==> {1}" 519 | .format(self.content, self.to)) 520 | self.hub.consume_delay(self.number) 521 | 522 | 523 | class GroupMsgRequest(WebQQRequest): 524 | 525 | """ 发送群消息 526 | 527 | :param group_uin: 群uin 528 | :param content: 消息内容 529 | """ 530 | url = "http://d.web2.qq.com/channel/send_qun_msg2" 531 | method = WebQQRequest.METHOD_POST 532 | 533 | def init(self, group_uin, content, style): 534 | self.delay, self.number = self.hub.get_delay(content) 535 | self.gid = self.hub.get_group_id(group_uin) 536 | self.group_uin = group_uin 537 | self.source = content 538 | content = self.hub.make_msg_content(content, style) 539 | r = {"group_uin": self.gid, "content": content, 540 | "msg_id": self.hub.msg_id, "clientid": self.hub.clientid, 541 | "psessionid": self.hub.psessionid} 542 | self.params = [("r", json.dumps(r)), 543 | ("psessionid", self.hub.psessionid), 544 | ("clientid", self.hub.clientid)] 545 | self.headers.update(Origin=const.D_ORIGIN, Referer=const.D_REFERER) 546 | 547 | def callback(self, resp, data): 548 | self.handle_retcode(data, u"[群消息] {0} ==> {1}" 549 | .format(self.source, self.group_uin)) 550 | self.hub.consume_delay(self.number) 551 | 552 | 553 | class DiscuListRequest(WebQQRequest): 554 | """ 获取讨论组列表 555 | """ 556 | url = "http://s.web2.qq.com/api/get_discus_list" 557 | 558 | def init(self): 559 | self.params = {"clientid": self.hub.clientid, 560 | "psessionid": self.hub.psessionid, 561 | "vfwebqq": self.hub.vfwebqq, 562 | "t": time.time() * 1000} 563 | self.headers.update(Referer=const.S_REFERER) 564 | 565 | def callback(self, resp, data): 566 | logger.info(u"[群列表] ==> {0!r}".format(data)) 567 | if data.get("retcode") == 0: 568 | self.hub.set_discu(data.get("result", {})) 569 | dids = self.hub.get_discu().dids 570 | for did in dids: 571 | self.hub.load_next_request(DiscuInfoRequest(did)) 572 | 573 | 574 | class DiscuInfoRequest(WebQQRequest): 575 | """ 获取讨论组详细信息 576 | 577 | :param did: 讨论组id 578 | """ 579 | url = "https://d.web2.qq.com/channel/get_discu_info" 580 | 581 | def init(self, did): 582 | self.params = {"clientid": self.hub.clientid, 583 | "did": did, 584 | "psessionid": self.hub.psessionid, 585 | "vfwebqq": self.hub.vfwebqq, 586 | "t": time.time() * 1000} 587 | self.headers.update(Referer=const.S_REFERER) 588 | self._did = did 589 | 590 | def callback(self, resp, data): 591 | if data.get("retcode") == 0: 592 | discu = self.hub.get_discu() 593 | logger.info(u"[讨论组] ==> {0} 的详细信息: {1}" 594 | .format(discu.get_name(self._did), data)) 595 | discu.set_detail(self._did, data.get("result", {})) 596 | 597 | 598 | class DiscuMsgRequest(WebQQRequest): 599 | """ 发送讨论组消息请求 600 | :param did: 讨论组id 601 | :param content: 消息内容 602 | """ 603 | 604 | url = "https://d.web2.qq.com/channel/send_discu_msg2" 605 | method = WebQQRequest.METHOD_POST 606 | 607 | def init(self, did, content, style): 608 | self.delay, self.number = self.hub.get_delay(content) 609 | self.did = did 610 | self.source = content 611 | content = self.hub.make_msg_content(content, style) 612 | r = {"did": did, "content": content, 613 | "msg_id": self.hub.msg_id, "clientid": self.hub.clientid, 614 | "psessionid": self.hub.psessionid} 615 | self.params = [("r", json.dumps(r)), 616 | ("psessionid", self.hub.psessionid), 617 | ("clientid", self.hub.clientid)] 618 | self.headers.update(Referer=const.D_REFERER) 619 | 620 | def callback(self, resp, data): 621 | self.handle_retcode(data, u"[讨论组消息] {0} ==> {1}" 622 | .format(self.source, self.did)) 623 | self.hub.consume_delay(self.number) 624 | 625 | 626 | class BuddyMsgRequest(WebQQRequest): 627 | 628 | """ 好友消息请求 629 | 630 | :param to_uin: 消息接收人 631 | :param content: 消息内容 632 | :param callback: 消息发送成功的回调 633 | """ 634 | url = "http://d.web2.qq.com/channel/send_buddy_msg2" 635 | method = WebQQRequest.METHOD_POST 636 | 637 | def init(self, to_uin, content, style): 638 | self.to_uin = to_uin 639 | self.source = content 640 | self.content = self.hub.make_msg_content(content, style) 641 | r = {"to": to_uin, "face": 564, "content": self.content, 642 | "clientid": self.hub.clientid, "msg_id": self.hub.msg_id, 643 | "psessionid": self.hub.psessionid} 644 | self.params = [("r", json.dumps(r)), ("clientid", self.hub.clientid), 645 | ("psessionid", self.hub.psessionid)] 646 | self.headers.update(Origin=const.D_ORIGIN) 647 | self.headers.update(Referer=const.S_REFERER) 648 | 649 | self.delay, self.number = self.hub.get_delay(content) 650 | 651 | def callback(self, resp, data): 652 | self.handle_retcode(data, u"[好友消息] {0} ==> {1}" 653 | .format(self.source, self.to_uin)) 654 | self.hub.consume_delay(self.number) 655 | 656 | 657 | class SetSignatureRequest(WebQQRequest): 658 | 659 | """ 设置个性签名请求 660 | 661 | :param signature: 签名内容 662 | """ 663 | url = "http://s.web2.qq.com/api/set_long_nick2" 664 | method = WebQQRequest.METHOD_POST 665 | 666 | def init(self, signature): 667 | self.params = (("r", json.dumps({"nlk": signature, 668 | "vfwebqq": self.hub.vfwebqq})),) 669 | self.headers.update(Origin=const.S_ORIGIN) 670 | self.headers.update(Referer=const.S_REFERER) 671 | 672 | def callback(self, resp, data): 673 | logger.info(u"[设置签名] {0}".format(data)) 674 | 675 | 676 | class AcceptVerifyRequest(WebQQRequest): 677 | 678 | """ 同意好友添加请求 679 | 680 | :param uin: 请求人uin 681 | :param qq_num: 请求人QQ号 682 | """ 683 | url = "http://s.web2.qq.com/api/allow_and_add2" 684 | method = WebQQRequest.METHOD_POST 685 | 686 | def init(self, uin, qq_num, markname=""): 687 | self.uin = uin 688 | self.qq_num = qq_num 689 | self.markname = markname 690 | self.params = [("r", "{\"account\":%d, \"gid\":0, \"mname\":\"%s\"," 691 | " \"vfwebqq\":\"%s\"}" % (qq_num, markname, 692 | self.hub.vfwebqq)), ] 693 | self.headers.update(Origin=const.S_ORIGIN) 694 | self.headers.update(Referer=const.S_REFERER) 695 | 696 | def callback(self, resp, data): 697 | if data.get("retcode") == 0: 698 | logger.info(u"[好友添加] 添加 {0} 成功".format(self.qq_num)) 699 | # if self.markname: 700 | # self.hub.mark_to_uin[self.markname] = self.uin 701 | else: 702 | logger.info(u"[好友添加] 添加 {0} 失败".format(self.qq_num)) 703 | 704 | 705 | class FileRequest(WebQQRequest): 706 | """ 下载传送的文件 707 | 708 | :param guid: 文件名 709 | :param lcid: 会话id 710 | :param to_uin: 发送人uin 711 | """ 712 | url = "http://d.web2.qq.com/channel/get_file2" 713 | 714 | def init(self, guid, lcid, to, callback=None): 715 | self.params = {"clientid": self.hub.clientid, 716 | "psessionid": self.hub.psessionid, 717 | "count": 1, "time": int(time.time() * 1000), 718 | "guid": guid, "lcid": lcid, "to": to} 719 | self.headers.update(Referer="http://web2.qq.com/webqq.html") 720 | self.headers.pop("Origin", None) 721 | self.fname = guid 722 | self._cb = callback 723 | 724 | def callback(self, response, data): 725 | """ 应该在客户端通过:: 726 | 727 | @register_request_handler(FileRequest) 728 | def callback(resp, data): 729 | pass 730 | 731 | 来实现文件的保存 732 | """ 733 | if self._cb: 734 | self._cb(self.fname, response.body) 735 | 736 | 737 | class LogoutRequset(WebQQRequest): 738 | """ 登出请求 739 | """ 740 | url = "https://d.web2.qq.com/channel/logout2" 741 | 742 | def init(self): 743 | self.params = {"clientid": self.hub.clientid, "ids": "", 744 | "psessionid": self.hub.psessionid, 745 | "t": int(time.time() * 1000)} 746 | self.headers.update(Referer=const.D_REFERER) 747 | 748 | def callback(self, resp, data): 749 | if data.get("retcode") == 0: 750 | logger.info(u"登出成功") 751 | 752 | 753 | FirstRequest = LoginSigRequest 754 | 755 | 756 | def _register_message_handler(func, args_func, msg_type="message"): 757 | """ 注册成功消息器 758 | 759 | :param func: 处理器 760 | :param args_func: 产生参数的处理器 761 | :param mst_type: 处理消息的类型 762 | """ 763 | func._twqq_msg_type = msg_type 764 | func._args_func = args_func 765 | return func 766 | 767 | 768 | def group_message_handler(func): 769 | """ 装饰处理群消息的函数 770 | 771 | 处理函数应接收5个参数: 772 | 773 | nickname 发送消息的群昵称 774 | content 消息内容 775 | group_code 群代码 776 | from_uin 发送人的uin 777 | source 消息原包 778 | """ 779 | 780 | def args_func(self, message): 781 | value = message.get("value", {}) 782 | gcode = value.get("group_code") 783 | uin = value.get("send_uin") 784 | contents = value.get("content", []) 785 | content = self.handle_qq_msg_contents(uin, contents, gcode) 786 | uname = self.get_group_member_nick(gcode, uin) 787 | return uname, content, gcode, uin, message 788 | 789 | return _register_message_handler(func, args_func, "group_message") 790 | 791 | 792 | def buddy_message_handler(func): 793 | """ 装饰处理好友消息的函数 794 | 795 | 处理函数应接收3个参数: 796 | 797 | from_uin 发送人uin 798 | content 消息内容 799 | source 消息原包 800 | """ 801 | 802 | def args_func(self, message): 803 | value = message.get("value", {}) 804 | from_uin = value.get("from_uin") 805 | contents = value.get("content", []) 806 | content = self.handle_qq_msg_contents(from_uin, contents) 807 | return from_uin, content, message 808 | return _register_message_handler(func, args_func, "message") 809 | 810 | 811 | def kick_message_handler(func): 812 | """ 装饰处理下线消息的函数 813 | 814 | 处理函数应接收1个参数: 815 | 816 | source 消息原包 817 | """ 818 | 819 | def args_func(self, message): 820 | return message, 821 | return _register_message_handler(func, args_func, "kick_message") 822 | 823 | 824 | def sess_message_handler(func): 825 | """ 装饰处理临时消息的函数 826 | 827 | 处理函数应接收3个参数: 828 | 829 | id 获取组签名的id 830 | from_uin 发送人uin 831 | content 消息内容 832 | source 消息原包 833 | """ 834 | 835 | def args_func(self, message): 836 | value = message.get("value", {}) 837 | id_ = value.get("id") 838 | from_uin = value.get("from_uin") 839 | contents = value.get("content", []) 840 | content = self.handle_qq_msg_contents(from_uin, contents) 841 | return id_, from_uin, content, message 842 | 843 | return _register_message_handler(func, args_func, "sess_message") 844 | 845 | 846 | def system_message_handler(func): 847 | """ 装饰处理系统消息的函数 848 | 849 | 处理函数应接手4个参数: 850 | 851 | type 消息类型 852 | from_uin 产生消息的人的uin 853 | account 产生消息的人的qq号 854 | source 消息原包 855 | """ 856 | 857 | def args_func(self, message): 858 | value = message.get('value') 859 | return (value.get("type"), value.get("from_uin"), value.get("account"), 860 | message) 861 | return _register_message_handler(func, args_func, "system_message") 862 | 863 | 864 | def discu_message_handler(func): 865 | """ 装饰处理讨论组消息的函数 866 | 867 | 处理函数应接收 868 | did 讨论组id 869 | from_uin 发送消息的人 870 | content 消息内容 871 | source 消息原包 872 | """ 873 | 874 | def args_func(self, message): 875 | value = message.get("value") 876 | from_uin = value.get("send_uin") 877 | did = value.get("did") 878 | content = self.handle_qq_msg_contents( 879 | from_uin, value.get("content", []), did, 1) 880 | return (did, from_uin, content, message) 881 | 882 | return _register_message_handler(func, args_func, "discu_message") 883 | 884 | 885 | def file_message_handler(func): 886 | """ 装饰处理文件消息的函数 887 | 888 | 处理函数应接受 889 | from_uin 文件发送人 890 | to_uin 文件接收人 891 | lcid 文件sessionid (此处为 session_id 字段) 892 | guid 文件名称 (此处为 name 字段) 893 | is_cancel 是否是取消发送 894 | source 消息源包 895 | """ 896 | def args_func(self, message): 897 | value = message.get("value", {}) 898 | return (value.get("from_uin"), value.get("to_uin"), 899 | value.get("session_id"), value.get("name"), 900 | value.get("cancel_type", None) == 1, 901 | message) 902 | return _register_message_handler(func, args_func, "file_message") 903 | 904 | 905 | def offline_file_message_handler(func): 906 | """ 装饰处理离线文件的函数 907 | 908 | 处理函数应接收 909 | from_uin 文件发送人 910 | to_uin 文件接收人 911 | lcid 文件sessionid (此处为 session_id 字段) 912 | count 离线文件数量 913 | file_infos 文件信息 914 | source 消息源包 915 | """ 916 | warnings.warn(u"WebQQ的离线消息并不可靠, 对方可能发送, 但是收不到") 917 | 918 | def args_func(self, message): 919 | value = message.get("value", {}) 920 | return (value.get("from_uin"), value.get("to_uin"), 921 | value.get("lcid"), value.get("count"), 922 | value.get("file_infos"), message) 923 | 924 | return _register_message_handler(func, args_func, "filesrv_transfer") 925 | 926 | 927 | def check_request(request): 928 | """ 检查Request参数是否合法, 并返回一个类对象 929 | """ 930 | if inspect.isclass(request): 931 | if not issubclass(request, WebQQRequest): 932 | raise ValueError("Request must be a subclass of WebQQRequest") 933 | elif isinstance(request, WebQQRequest): 934 | request = request.__class__ 935 | else: 936 | raise ValueError( 937 | "Request must be a subclass or instance of WebQQRequest") 938 | 939 | return request 940 | 941 | 942 | def register_request_handler(request): 943 | """ 返回一个装饰器, 用于装饰函数,注册为Request的处理函数 944 | 处理函数需接收两个参数: 945 | 946 | request 本次请求的实例 947 | response 相应 ~tornado.httpclient.HTTPResponse instance 948 | data response.body or dict 949 | 950 | :param request: 请求类或请求实例 951 | :type request: WebQQRequest or WebQQRequest instance 952 | :rtype: decorator function 953 | """ 954 | def wrap(func): 955 | func._twqq_request = check_request(request) 956 | return func 957 | return wrap 958 | --------------------------------------------------------------------------------