├── 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'
\n','')
52 |
53 | city_aqi_update_time = city_aqi_update_array.replace('
', '').strip()
54 | city_aqi_update_time = city_aqi_update_time.replace('\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 |
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 |
--------------------------------------------------------------------------------