├── README.md ├── dev ├── get_friend_info2.js.txt ├── get_group_info_ext2.js.txt ├── get_user_friends2.js.txt └── mq_private.js.txt └── src ├── example.py ├── mlogger.py ├── qqfriends.py ├── qqhttp.py ├── qqhttp_gevent.py └── qqrobot.py /README.md: -------------------------------------------------------------------------------- 1 | # pyQQRobot\[discontinued\] 2 | **Project discontinued due to abolishment of WebQQ service.** 3 | 4 | 基于Python3与WebQQ的QQ机器人框架。 5 | 6 | A QQ Robot framework based on WebQQ and Python3. 7 | 8 | ## How to use? 9 | Here is a simple example. 10 | 11 | ```python 12 | #!/usr/bin/env python3 13 | 14 | from qqrobot import QQClient, QQHandler 15 | import mlogger as log 16 | 17 | class MyHandler(QQHandler): 18 | def on_buddy_message(self, uin, msg): 19 | log.i('QQ', 'got message from ' + str(uin)) 20 | self.send_buddy_message(uin, "Powered by pyQQRobot.") 21 | 22 | if __name__ == '__main__': 23 | client = QQClient() 24 | # to use a gevent-based client, use: 25 | # from qqhttp_gevent import mHTTPClient_gevent 26 | # client = QQClient(HTTPClient=mHTTPClient_gevent) 27 | log_or_load = 'log' 28 | 29 | if log_or_load == 'log': 30 | # you can log in manually 31 | client.QR_veri() 32 | client.login() 33 | # and then save your verification 34 | client.save_veri('./' + client.uin + '.veri') 35 | elif log_or_load == 'load': 36 | # or load from a file instead 37 | client.load_veri('./my_verification.veri') 38 | # You don't need to fetch all that lists, 39 | # as they are already loaded from verfication files. 40 | client.login(get_info=False) 41 | 42 | # create & add a message handler 43 | h = MyHandler() 44 | client.add_handler(h) 45 | # then start your journey... 46 | client.listen() 47 | ``` 48 | 49 | You can refer to `src/example.py` for another example, using some sort of intelligent robot to make responses to messages. 50 | 51 | ## Functions 52 | Now you can: 53 | 54 | * send messages to your buddies and groups 55 | * set listeners to messages from your buddies and groups 56 | * get information about yourself, your buddies and groups 57 | 58 | Discuss groups aren't supported. 59 | 60 | ## Structure 61 | Here's a brief list of the files included: 62 | 63 | * **qqrobot.py** includes 64 | * **QQClient** A set of WebQQ APIs and the core runtime of pyQQRobot. 65 | * **QQHandler** The simple plugin framework. 66 | * **qqfriends.py** The QQ friends, groups and discus groups data parser. 67 | * **qqhttp.py** Simple HTTP Client. See also **qqhttp_gevent.py**, one uses gevent. 68 | * **mlogger.py** Simple screen logger. 69 | 70 | ## Disclaimer 71 | As a **Senior Three student in China** there just cannot be enough time for me to maintain the project. I sincerely hope that there'll be coders interested to help. 72 | 73 | ## so, what TODO next? 74 | Well, no maintainance guaranteed. Maybe I'll turn a blind eye to issues and PRs, but if you insist, just send one. 75 | 76 | 1. **`QQClient` itself is a little messy.** To solve this, I think `qqhttp` should be dumped, and take an ordinary way - like `requests`. Concurrency and asynchronization can be implemented with `gevent` or other libraries. 77 | 2. **Sending messages to discus groups.** Technically not difficult. 78 | 3. **Finding friends.** Until now I still couldn't find an existing interface in WebQQ protocol to determine a specified user by his/her account number(QQ号). Sure there's way to do it(I already have something in mind). 79 | -------------------------------------------------------------------------------- /dev/get_friend_info2.js.txt: -------------------------------------------------------------------------------- 1 | { 2 | "retcode": 0, 3 | "result": { 4 | "face": 123, //face no. 5 | "birthday": { 6 | "month": 1, 7 | "year": 2000, 8 | "day": 1 9 | }, 10 | "occupation": "", 11 | "phone": "", 12 | "allow": 1, 13 | "college": "", 14 | "uin": 123456789, 15 | "constel": 12, 16 | "blood": 0, 17 | "homepage": "", 18 | "stat": 20, 19 | "vip_info": 0, 20 | "country": "user_country", 21 | "city": "user_city", 22 | "personal": "", 23 | "nick": "user_nickname", 24 | "shengxiao": 1, 25 | "email": "user_email", 26 | "province": "user_province", 27 | "gender": "user_gender", 28 | "mobile": "user_mobile_number" 29 | } 30 | } -------------------------------------------------------------------------------- /dev/get_group_info_ext2.js.txt: -------------------------------------------------------------------------------- 1 | { 2 | "retcode": 0, 3 | "result": { 4 | "stats": [ 5 | { 6 | "client_type": 1, 7 | "uin": 123456789, //user uin 8 | "stat": 10 //what is this? 9 | }, 10 | // and more info like that.... 11 | ], 12 | "minfo": [ 13 | { 14 | "nick": "user_nickname", 15 | "province": "user_province", 16 | "gender": "user_gender", 17 | "uin": 1234567890, //user uin 18 | "country": "user_country", 19 | "city": "user_city" 20 | }, 21 | // and more info like that... 22 | ], 23 | "ginfo": { 24 | "face": 0, 25 | "memo": "group_introduction", 26 | "class": 25, //group class 27 | "fingermemo": "", 28 | "code": 123456789, //group code? 29 | "createtime": 100000000, //create time in unix time stamp format 30 | "flag": 123456789, //what is that all about? 31 | "level": 0, //group level 32 | "name": "group_name", 33 | "gid": 123456789, 34 | "owner": 123456789, 35 | "members": [ 36 | { 37 | "muin": 123456789, //don't think too much, it's exactly the uin 38 | "mflag": 0 39 | }, 40 | // and more info like that... 41 | ], 42 | "option": 2 //what is that all about? 43 | }, 44 | "cards": [ 45 | { 46 | "muin": 123456789, 47 | "card": "my_namecard_in_this_group" 48 | }, 49 | // and more info like that... 50 | ], 51 | "vipinfo": [ 52 | { 53 | "vip_level": 7, 54 | "u": 123456789, 55 | "is_vip": 1 //which means this guy is vip 56 | }, 57 | // and more info like that.... 58 | ] 59 | } 60 | } -------------------------------------------------------------------------------- /dev/get_user_friends2.js.txt: -------------------------------------------------------------------------------- 1 | { 2 | "retcode": 0, 3 | "result": { 4 | "friends": [ 5 | { 6 | "flag": 8, //don't know what that means 7 | "uin": 123456789, 8 | "categories": 1 //category number in **index** 9 | }, 10 | // and more info like that... 11 | ], 12 | "marknames": [ 13 | { 14 | "uin": 123456789, 15 | "markname": "user_markname", 16 | "type": 0 //always zero 17 | }, 18 | ], 19 | "categories": [ 20 | { 21 | "index": 1, //which goes in turn... 22 | "sort": 1, //sorted order... 23 | "name": "category_name" 24 | }, 25 | // and more info like that... 26 | ], 27 | "vipinfo": [ 28 | { 29 | "vip_level": 0, //no vip level 30 | "u": 123456789, 31 | "is_vip": 0 //which means this guy isn't vip 32 | }, 33 | // and more info like that... 34 | ], 35 | "info": [ 36 | { 37 | "face": 201, //what is that all about? 38 | "flag": 294126144, //what is that all about? 39 | "nick": "user_nickname", 40 | "uin": 123456789 41 | }, 42 | // and more info like that... 43 | ] 44 | } 45 | } -------------------------------------------------------------------------------- /src/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | from urllib import request, parse 5 | 6 | from qqrobot import QQClient, QQHandler 7 | import mlogger as log 8 | 9 | 10 | class QQTulingHandler(QQHandler): 11 | url_req = "http://www.tuling123.com/openapi/api" 12 | 13 | def __init__(self, APIKey=None): 14 | if str(APIKey) in ('None', ''): 15 | print('You\'ll have to provide an APIKey first.') 16 | print('Get it @ http://tuling123.com') 17 | raise ValueError('APIKey not available') 18 | else: 19 | self.key = APIKey 20 | 21 | def on_buddy_message(self, uin, msg): 22 | d = parse.urlencode( 23 | {'key': self.key, 'info': msg, 'userid': uin}).encode('utf-8') 24 | with request.urlopen(self.url_req, data=d) as f: 25 | j = json.loads(f.read().decode('utf-8')) 26 | log.i('Tuling', ':'.join((str(uin), msg))) 27 | log.i('Tuling', 'response:' + j['text']) 28 | self.send_buddy_message(uin, j['text']) 29 | 30 | 31 | if __name__ == "__main__": 32 | a = QQClient() 33 | h = QQTulingHandler(input('API Key:')) 34 | 35 | # you can save your verification 36 | a.QR_veri() 37 | a.login() 38 | # a.login(save_veri=True) to save verfication file when 39 | # login succeeded, or use the following method when 40 | # you want to save the verification file. 41 | a.save_veri() # default filename will be ./`QQClient.uin`.veri 42 | 43 | # or load from a file instead 44 | # a.load_veri('/path/to/your/verification') 45 | # a.login(get_info=False) 46 | 47 | a.add_handler(h) 48 | a.listen(join=True) 49 | -------------------------------------------------------------------------------- /src/mlogger.py: -------------------------------------------------------------------------------- 1 | import time 2 | from functools import partial 3 | import json 4 | 5 | class Enum(set): 6 | def __getattr__(self, name): 7 | if name in self: 8 | return name 9 | raise AttributeError 10 | 11 | levels = Enum(('w', 'i', 'e', 'v')) 12 | 13 | _supressed_tags = set() 14 | _supressed_levels = set() 15 | _sav = [] 16 | 17 | def log(tag, s, pretty, level, save=True): 18 | if save: 19 | _sav.append((time.strftime('%H:%M:%S'), level, tag, s)) 20 | if tag in _supressed_tags or level in _supressed_levels: 21 | return 22 | print(pretty.format(time=time.strftime('%H:%M:%S'), tag=tag, msg=s)) 23 | 24 | pretty_warning = ( 25 | '\033[43;30mWARN\033[0m {time} \033[4m{tag}\033[0;1m\t{msg}\033[0m') 26 | pretty_information = ( 27 | '\033[42;30mINFO\033[0m {time} \033[4m{tag}\033[0;1m\t{msg}\033[0m') 28 | pretty_error = ( 29 | '\033[41;37mEROR\033[0m {time} \033[4m{tag}\033[0;1m\t{msg}\033[0m') 30 | pretty_verbose = ( 31 | '\033[47;30mVERB\033[0m {time} \033[4m{tag}\033[0;1m\t{msg}\033[0m') 32 | 33 | w = partial(log, pretty=pretty_warning, level=levels.w) 34 | i = partial(log, pretty=pretty_information, level=levels.i) 35 | e = partial(log, pretty=pretty_error, level=levels.e) 36 | v = partial(log, pretty=pretty_verbose, level=levels.v) 37 | 38 | def supress_tag(tag): 39 | _supressed_tags.add(tag) 40 | 41 | def supress_level(level): 42 | _supressed_levels.add(level) 43 | 44 | def unsupress_tag(tag): 45 | _supressed_tags.discard(tag) 46 | 47 | def unsupress_level(level): 48 | _supressed_levels.discard(level) 49 | 50 | def unsupress_all_tags(): 51 | _supressed_tags.clear() 52 | 53 | def unsupress_all_levels(): 54 | _supressed_levels.clear() 55 | 56 | def output(*s, sep=' ', end='\n', file=None, flush=False): 57 | v('print', sep.join(map(lambda x: str(x), s))) 58 | 59 | def save(filename=None, supressed_tags=None, 60 | supressed_levels=None, prompt=True): 61 | if filename == None: 62 | filename = './mlogger_%s.log' % (time.strftime('%Y_%m_%d_%H_%M_%S')) 63 | if (supressed_tags or supressed_levels) == None: 64 | with open(filename, 'w') as f: 65 | json.dump(_sav, f, ensure_ascii=False, indent=4) 66 | else: 67 | if supressed_levels == 'default': 68 | supressed_levels = _supressed_levels 69 | if supressed_tags == 'default': 70 | supressed_tags = _supressed_tags 71 | supressed_levels = supressed_levels or () 72 | supressed_tags = supressed_tags or () 73 | 74 | s = [i for i in _sav 75 | if not(i[1] in supressed_levels or i[2] in supressed_tags)] 76 | with open(filename, 'w') as f: 77 | json.dump(s, f, ensure_ascii=False, indent=4) 78 | if prompt: 79 | i('logger', 'Save file created: '+filename, save=False) 80 | 81 | 82 | if __name__ == '__main__': 83 | # for test only 84 | w('tag', 'this is a warning') 85 | i('tag', 'this is an information') 86 | e('tag', 'this is an error') 87 | v('tag', 'this is a verbose man') 88 | 89 | i('tag', 'you can see me here but not in the save file.', save=False) 90 | supress_level(levels.w) 91 | w('tag', 'you cannot see me') 92 | 93 | supress_tag('awful') 94 | i('awful', 'you cannot see me') 95 | 96 | unsupress_level(levels.w) 97 | unsupress_tag('awful') 98 | w('awful', 'glad you can see me') 99 | 100 | save() 101 | -------------------------------------------------------------------------------- /src/qqfriends.py: -------------------------------------------------------------------------------- 1 | class QQFriends(): 2 | def __init__(self): 3 | self.f = {} # friends list 4 | self.g = {} # group list 5 | self.d = {} # discus group list 6 | self.r = {} # recent list 7 | self.c = [] 8 | self.group_info = {} 9 | self.user_info = {} 10 | 11 | def parse_friends(self, j): 12 | j = j['result'] 13 | # category, uin & flag 14 | for e in j['friends']: 15 | self.f[e['uin']] = {} 16 | self.f[e['uin']]['category'] = e['categories'] 17 | self.f[e['uin']]['flag'] = e['flag'] 18 | 19 | # marknames 20 | for e in j['marknames']: 21 | self.f[e['uin']]['markname'] = e['markname'] 22 | # self.f[e['uin']]['mType'] = e['type'] 23 | 24 | # vipinfo 25 | for e in j['vipinfo']: 26 | self.f[e['u']]['vip'] = e['is_vip'] and e['vip_level'] 27 | 28 | # user info 29 | for e in j['info']: 30 | self.f[e['uin']]['nickname'] = e['nick'] 31 | self.f[e['uin']]['face'] = e['face'] 32 | self.f[e['uin']]['flag'] = e['flag'] 33 | 34 | # categories 35 | for e in j['categories']: 36 | self.c.append(e['name']) 37 | 38 | def parse_groups(self, j): 39 | j = j['result'] 40 | self.g = {e['gid']: e for e in j['gnamelist']} 41 | for e in self.g.values(): 42 | del(e['gid']) 43 | 44 | def parse_discus(self, j): 45 | j = j['result'] 46 | self.d = {e['did']: e for e in j['dnamelist']} 47 | for e in self.d.values(): 48 | del(e['did']) 49 | 50 | def parse_recent(self, j): 51 | j = j['result'] 52 | self.r[0] = [] 53 | self.r[1] = [] 54 | self.r[2] = [] 55 | for b in j: 56 | self.r[b['type']].append(b['uin']) 57 | 58 | def parse_online_buddies(self, j): 59 | pass 60 | 61 | def get_group_info(self, gid): 62 | return self.group_info.get(gid) 63 | 64 | def parse_group_info(self, j): 65 | j = j['result'] 66 | g = j['ginfo'] 67 | g['members'] = {m['muin']: {} for m in g['members']} # ignore mflags 68 | for m in j['stats']: 69 | g['members'][m['uin']].update(m) 70 | for m in j['minfo']: 71 | g['members'][m['uin']].update(m) 72 | for m in j['cards']: 73 | g['members'][m['muin']].update(m) 74 | for m in j['vipinfo']: 75 | g['members'][m['u']]['vip_level'] = ( 76 | m['is_vip'] and m['vip_level']) 77 | for m in g['members'].values(): 78 | m.pop('uin', None) 79 | m.pop('muin', None) 80 | m.pop('u', None) 81 | self.group_info[g['gid']] = g 82 | return g 83 | 84 | def get_user_info(self, uin): 85 | return self.user_info.get(uin) 86 | 87 | def parse_user_info(self, j): 88 | self.user_info[j['result']['uin']] = j['result'] 89 | return j['result'] -------------------------------------------------------------------------------- /src/qqhttp.py: -------------------------------------------------------------------------------- 1 | import json 2 | import traceback 3 | from multiprocessing.dummy import Pool 4 | from urllib import request, parse 5 | from http.cookiejar import Cookie, MozillaCookieJar 6 | 7 | import mlogger as log 8 | 9 | 10 | class mHTTPClient(object): 11 | str_cb_unclear = 'Callback method not specified.' 12 | 13 | def req(self, url, *, data=None, headers={}): 14 | raise NotImplementedError() 15 | 16 | def req_async(self, url, *, data=None, headers={}, cb=None): 17 | if cb is None: 18 | raise ValueError(self.str_cb_unclear) 19 | cb(self.req(url, data=data, headers=headers), 20 | {'url': url, 'data': data, 'headers': headers}) 21 | 22 | # ----------syntactic sugars------------ 23 | 24 | def get_json(self, url, *, data=None, headers={}): 25 | return json.loads(self.get_text(url, data=data, headers=headers)) 26 | 27 | def get_text(self, url, *, data=None, headers={}): 28 | return self.req( 29 | url, data=data, headers=headers).decode('utf-8').strip() 30 | 31 | def get_image(self, url, *, data=None, filename='./tmp', headers={}): 32 | with open(filename, 'wb') as f: 33 | f.write(self.req(url, data=data, headers=headers)) 34 | return filename 35 | 36 | def get_text_async(self, url, *, data=None, headers={}, cb=None): 37 | if cb is None: 38 | raise ValueError(self.str_cb_unclear) 39 | 40 | def cb_text(x, y): 41 | return cb(x.decode('utf-8').strip(), y) 42 | self.req_async(url, data=data, headers=headers, cb=cb_text) 43 | 44 | def get_json_async(self, url, *, data=None, headers={}, cb=None): 45 | if cb is None: 46 | raise ValueError(self.str_cb_unclear) 47 | 48 | def cb_json(x, y): 49 | return cb(json.loads(x.decode('utf-8')), y) 50 | self.req_async(url, data=data, headers=headers, cb=cb_json) 51 | 52 | # --------end syntactic sugars--------- 53 | 54 | def set_cookie(self, name, value, domain, expires=None): 55 | raise NotImplementedError() 56 | 57 | def get_cookie(self, name, domain): 58 | raise NotImplementedError() 59 | 60 | def get_cookies(self): 61 | raise NotImplementedError() 62 | 63 | def clear_cookie(self): 64 | raise NotImplementedError() 65 | 66 | 67 | class mHTTPClient_urllib(mHTTPClient): 68 | def req(self, url, *, data=None, headers={}): 69 | r = request.Request(url) 70 | if data is not None: 71 | r.data = parse.urlencode(data).encode('utf-8') 72 | r.headers.update(headers) 73 | try: 74 | f = self.opener.open(r) 75 | return f.read() 76 | except Exception: 77 | log.w('http', 'HTTP error @ ' + url) 78 | traceback.print_exc() 79 | 80 | 81 | def req_async(self, url, *, data=None, headers={}, cb=None): 82 | if cb is None: 83 | raise ValueError(self.str_cb_unclear) 84 | 85 | def task(): 86 | cb(self.req(url, data=data, headers=headers), 87 | {'url': url, 'data': data, 'headers': headers}) 88 | self.threadPool.apply_async(task) 89 | 90 | def set_cookie(self, name, value, domain, path='/', expires=None): 91 | self.cj.set_cookie(Cookie( 92 | version=0, name=name, value=value, port=None, 93 | port_specified=False, domain=domain, domain_specified=True, 94 | domain_initial_dot=False, path=path, path_specified=True, 95 | secure=False, expires=expires, discard=False, comment=None, 96 | comment_url=None, rest=None)) 97 | 98 | def get_cookie(self, name, domain, path='/'): 99 | try: 100 | return self.cj._cookies[domain][path][name].value 101 | except Exception: 102 | raise RuntimeError('Cookie not found:%s @ %s%s' % ( 103 | name, domain, path)) 104 | 105 | def get_cookies(self): 106 | '''get all the cookies saved. 107 | path is ignored here, as it's always '/'(root) 108 | ''' 109 | d = {} 110 | for domain, cookieDict in self.cj._cookies.items(): 111 | d[domain] = {} 112 | for path, cks in cookieDict.items(): 113 | d[domain].update({n: ck.value for n, ck in cks.items()}) 114 | return d 115 | 116 | def clear_cookie(self): 117 | self.cj.clear() 118 | 119 | def __init__(self): 120 | self.cj = MozillaCookieJar() 121 | self.opener = request.build_opener( 122 | request.HTTPCookieProcessor(self.cj)) 123 | self.threadPool = Pool(processes=10) 124 | -------------------------------------------------------------------------------- /src/qqhttp_gevent.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | import gevent 4 | from gevent import monkey 5 | monkey.patch_all() 6 | 7 | from urllib import request, parse 8 | from qqhttp import mHTTPClient_urllib 9 | 10 | import mlogger as log 11 | 12 | class mHTTPClient_gevent(mHTTPClient_urllib): 13 | def __init__(self): 14 | # mHTTPClient running gevent 15 | # during test 16 | log.w('http', "You're using gevent based http client.") 17 | super(mHTTPClient_gevent, self).__init__() 18 | 19 | def _gevent_req(self, url, data, headers): 20 | r = request.Request(url) 21 | if data is not None: 22 | r.data = parse.urlencode(data).encode('utf-8') 23 | r.headers.update(headers) 24 | try: 25 | f = self.opener.open(r) 26 | # hang up here 27 | # gevent will automatically switch to different jobs 28 | return f.read() 29 | except Exception: 30 | log.w('http', 'HTTP error @ ' + url) 31 | traceback.print_exc() 32 | 33 | def req(self, url, *, data=None, headers={}): 34 | # spawn a job and join 35 | j = gevent.spawn(self._gevent_req, url, data=data, headers=headers) 36 | j.join() # timeout = None 37 | return j.value 38 | 39 | def req_async(self, url, *, data=None, headers={}, cb=None): 40 | if cb is None: 41 | raise ValueError(self.str_cb_unclear) 42 | 43 | # new callback with a greenlet object as the argument 44 | def gevent_cb(g): 45 | return cb(g.value, {'url': url, 'data': data, 'headers': headers}) 46 | 47 | j = gevent.spawn(self._gevent_req, url, data=data, headers=headers) 48 | j.link(gevent_cb) 49 | j.join(timeout = 0) # don't wait and exit -------------------------------------------------------------------------------- /src/qqrobot.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import threading 4 | import traceback 5 | from os import system as exec_cmd 6 | from platform import system as get_sys_name 7 | 8 | from qqfriends import QQFriends 9 | from qqhttp_gevent import mHTTPClient_gevent 10 | import mlogger as log 11 | 12 | 13 | def utime(): 14 | return int(time.time()) 15 | 16 | # WARN: the following command set default `print` to 17 | # mLogger output command. 18 | # It's suggested to use log.v(TAG, 'message....') 19 | # instead of using `print` directly. 20 | # print = log.output 21 | 22 | 23 | class QQClient(): 24 | default_headers = dict( 25 | Referer='http://s.web2.qq.com/proxy.html', 26 | User_Agent=( 27 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/' 28 | '537.36 (KHTML, like Gecko) Chrome/50.0.2661.86 Safari/537.36')) 29 | poll_headers = dict( 30 | Origin='http://d1.web2.qq.com', 31 | Referer='http://d1.web2.qq.com/proxy.html', 32 | User_Agent=( 33 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/' 34 | '537.36 (KHTML, like Gecko) Chrome/50.0.2661.86 Safari/537.36')) 35 | 36 | def __init__(self, HTTPClient=mHTTPClient_gevent, handlers=[]): 37 | self.friend_list = QQFriends() 38 | self.http_client = HTTPClient() 39 | self.msg_id = 50500000 40 | self.handlers = handlers 41 | 42 | def _callback_receive(self, resp, previous): 43 | tag = 'listener' 44 | try: 45 | resp = json.loads(resp.decode('utf-8')) 46 | if resp.get('retcode', 0) != 0: 47 | # something is wrong 48 | log.e(tag, 'error retcode %d errmsg %s' % ( 49 | resp.get('retcode', 0), resp.get('errmsg', 'none'))) 50 | if resp.get('retcode', 0) == 103: 51 | # Connection failed, log in again. 52 | log.w(tag, 'Meet with error 103.') 53 | log.w(tag, 'You\'ll have to log in again.') 54 | exit() 55 | else: 56 | if resp.get('retcode') != 0: 57 | # something is really wrong 58 | log.e(tag, 'retcode %s errmsg %s' % ( 59 | resp.get('errmsg'),resp.get('retcode'))) 60 | return 61 | 62 | if resp.get('errmsg') == 'error!!!': 63 | # {errmsg: "error!!!", retcode: 0} 64 | # http connection time out 65 | # ignore it and continue listening 66 | return 67 | 68 | # parse result 69 | for c in resp['result']: 70 | num = c['value']['from_uin'] # uin or gid 71 | msg = c['value']['content'][1] # content 72 | if c['poll_type'] == 'message': 73 | for h in self.handlers: 74 | h.on_buddy_message(num, msg) 75 | elif c['poll_type'] == 'group_message': 76 | uin = c['value']['send_uin'] # uin 77 | for h in self.handlers: 78 | h.on_group_message(num, uin, msg) 79 | except Exception: 80 | log.e(tag, 'Fatal error parsing messages.') 81 | log.e(tag, 'Response: ' + str(resp)) 82 | traceback.print_exc() 83 | 84 | def _callback_send(self, resp, previous): 85 | tag = 'sender' 86 | resp = json.loads(resp.decode('utf-8')) 87 | if resp.get('errCode', 0) != 0 or resp.get('retcode', 0) != 0: 88 | log.w(tag, 'The following error occurred when sending last message:') 89 | if resp.get('retcode', 0) == 1202: 90 | # retcode 1202: ignored, as WebQQ itself has ignored it too. 91 | log.w(tag, '\tretcode 1202') 92 | log.w(tag, '\tMessage could be lost, but usually not big deal.') 93 | 94 | def _parse_arg(self, js_str): 95 | js_str = js_str[js_str.index('(') + 1: len(js_str) - 2] 96 | return list(map(lambda x: x.strip().strip("'"), js_str.split(','))) 97 | 98 | def get_qq_hash(self): 99 | # rewritten from an javascript function 100 | # see mq_private.js for original version 101 | if not hasattr(self, '_qhash'): 102 | x = int(self.uin) 103 | I = self.ptwebqq 104 | N = [0, 0, 0, 0] 105 | i = 0 106 | while i < len(I): 107 | N[i % 4] ^= ord(I[i]) 108 | i += 1 109 | V = [] 110 | V.append(x >> 24 & 255 ^ ord('E')) 111 | V.append(x >> 16 & 255 ^ ord('C')) 112 | V.append(x >> 8 & 255 ^ ord('O')) 113 | V.append(x & 255 ^ ord('K')) 114 | U = [] 115 | for T in range(8): 116 | if T % 2 == 0: 117 | U.append(N[T >> 1]) 118 | else: 119 | U.append(V[T >> 1]) 120 | N = ["0", "1", "2", "3", "4", "5", "6", "7", 121 | "8", "9", "A", "B", "C", "D", "E", "F"] 122 | V = "" 123 | for T in range(len(U)): 124 | V += N[U[T] >> 4 & 15] 125 | V += N[U[T] & 15] 126 | self._qhash = V 127 | return self._qhash 128 | 129 | def QR_veri(self, show_QR=None): 130 | tag = 'verify' 131 | # --------------necessary urls-------------- 132 | url_get_QR_image = "https://ssl.ptlogin2.qq.com/ptqrshow?" \ 133 | "appid=501004106&e=0&l=M&s=5&d=72&v=4&t=0.5" 134 | url_check_QR_state = ( 135 | "https://ssl.ptlogin2.qq.com/ptqrlogin?webqq_type=10&" 136 | "remember_uin=1&login2qq=1&aid=501004106&u1=" 137 | "http%3A%2F%2Fw.qq.com%2Fproxy.html%3Flogin2qq%3D1%26webqq_type" 138 | "%3D10&ptredirect=0&ptlang=2052&daid=164&from_ui=1&pttype=1&" 139 | "dumy=&fp=loginerroralert&action=0-0-{timer}&mibao_css=m_webqq&" 140 | "t=undefined&g=1&js_type=0&js_ver=10139&login_sig=&pt_randsalt=0" 141 | ) 142 | # ------------end necessary urls------------ 143 | 144 | # get QR image 145 | if show_QR is None: 146 | def e(): 147 | f = self.http_client.get_image(url_get_QR_image) 148 | s = get_sys_name() 149 | if s == 'Darwin': # Mac OSX 150 | exec_cmd('open ' + f) 151 | elif s == 'Windows': # Windows 152 | exec_cmd(f) 153 | else: # Linux or whatever(GUI availability unknown) 154 | pass 155 | log.i(tag, 'QR Image saved @ ' + f) 156 | show_QR = e 157 | show_QR() 158 | 159 | # check QR verification state 160 | t = int(time.clock() * 10000) + 10000 # default clock 161 | prev = -1 162 | while True: 163 | time.sleep(1) 164 | t += int(time.clock() * 10000) 165 | res = self._parse_arg( 166 | self.http_client.get_text(url_check_QR_state.format(timer=t))) 167 | if prev != res[0]: 168 | if res[0] == '65': 169 | log.i(tag, 'QR code expired.') 170 | show_QR() 171 | elif res[0] == '66': 172 | log.i(tag, 'Please scan the QRCode shown on your screen.') 173 | elif res[0] == '67': 174 | log.i(tag, 'Please press confirm on your phone.') 175 | elif res[0] == '0': 176 | # QR code verification completed 177 | log.i(tag, res[-2]) 178 | self.username = res[-1] 179 | log.i(tag, 'User name: ' + self.username) 180 | break 181 | prev = res[0] 182 | 183 | # first step login 184 | self.http_client.req(res[2]) 185 | 186 | # cookie proxy 187 | self.http_client.set_cookie( 188 | 'p_skey', 189 | self.http_client.get_cookie('p_skey', '.web2.qq.com'), 190 | 'w.qq.com') 191 | self.http_client.set_cookie( 192 | 'p_uin', 193 | self.http_client.get_cookie('p_uin', '.web2.qq.com'), 194 | 'w.qq.com') 195 | 196 | def login(self, get_info=True, save_veri=False, filename=None): 197 | tag = 'login' 198 | # --------necessary urls & data-------- 199 | url_get_vfwebqq = "http://s.web2.qq.com/api/getvfwebqq?" \ 200 | "ptwebqq={ptwebqq}&psessionid=&t=1456633306528" 201 | url_login2 = "http://d1.web2.qq.com/channel/login2" 202 | post_login2 = {'clientid': 53999199, 203 | 'pssessionid': '', 'status': 'online'} 204 | # ------end necessary urls & data------ 205 | 206 | # get ptwebqq 207 | self.ptwebqq = self.http_client.get_cookie('ptwebqq', '.qq.com') 208 | # get vfwebqq 209 | self.vfwebqq = self.http_client.get_json( 210 | url_get_vfwebqq.format(ptwebqq=self.ptwebqq), 211 | headers=self.default_headers)['result']['vfwebqq'] 212 | 213 | # second step login 214 | post_login2['ptwebqq'] = self.ptwebqq 215 | j2 = self.http_client.get_json( 216 | url_login2, data={'r': json.dumps(post_login2)}) 217 | 218 | self.uin = j2['result']['uin'] 219 | self.psessionid = j2['result']['psessionid'] 220 | self.status = j2['result']['status'] 221 | self.get_qq_hash() 222 | if get_info: 223 | self.get_user_friends() 224 | self.get_group_list() 225 | self.get_discus_list() 226 | if save_veri: 227 | log.i(tag, 'Verification saved @ ' + self.save_veri(filename)) 228 | self.get_online_buddies() 229 | self.get_recent_list() 230 | 231 | def save_veri(self, filename=None): 232 | if filename is None: 233 | filename = './' + str(self.uin) + '.veri' 234 | 235 | with open(filename, 'w') as f: 236 | # save all cookies 237 | f.write('{"cookies":') 238 | json.dump(self.http_client.get_cookies(), f) 239 | # save username 240 | f.write(',\n"username":"%s"' % self.username) 241 | # save user friends, groups and discus groups 242 | f.write(',\n"friends":') 243 | json.dump(self.friend_list.f, f) 244 | f.write(',\n"groups":') 245 | json.dump(self.friend_list.g, f) 246 | f.write(',\n"discus_groups":') 247 | json.dump(self.friend_list.d, f) 248 | f.write('}') 249 | 250 | return filename 251 | 252 | def load_veri(self, filename): 253 | tag = 'verify' 254 | with open(filename, 'r') as f: 255 | v = json.load(f) 256 | for domain, cookies in v['cookies'].items(): 257 | for name, value in cookies.items(): 258 | self.http_client.set_cookie(name, value, domain) 259 | self.username = v['username'] 260 | # TODO deal with the int-key conversion issues 261 | self.friend_list.f = { 262 | int(id): value for id, value in v['friends'].items()} 263 | self.friend_list.g = { 264 | int(id): value for id, value in v['groups'].items()} 265 | self.friend_list.d = { 266 | int(id): value for id, value in v['discus_groups'].items()} 267 | log.i(tag, 'Verification loaded from ' + filename) 268 | log.i(tag, 'Username: ' + self.username) 269 | 270 | def listen(self, join=False): 271 | url_poll2 = 'http://d1.web2.qq.com/channel/poll2' 272 | d = {'r': json.dumps({ 273 | "ptwebqq": self.ptwebqq, "clientid": 53999199, 274 | "psessionid": self.psessionid, "key": ""})} 275 | 276 | def l(): 277 | log.i('listener', 'Listener thread started.') 278 | while True: 279 | r = self.http_client.req( 280 | url_poll2, data=d, headers=self.poll_headers) 281 | self._callback_receive( 282 | r, {'url': url_poll2, 283 | 'data': d, 'headers': self.poll_headers}) 284 | 285 | t = threading.Thread(name='qq_client_listener', target=l) 286 | t.start() 287 | if join: 288 | t.join() 289 | 290 | def get_user_friends(self): 291 | self.friend_list.parse_friends(self.http_client.get_json( 292 | 'http://s.web2.qq.com/api/get_user_friends2', 293 | data={'r': json.dumps({ 294 | 'hash': self.get_qq_hash(), 295 | 'vfwebqq': self.vfwebqq})}, 296 | headers=self.default_headers)) 297 | log.i('list', 'Finished getting friend list.') 298 | 299 | def get_group_list(self): 300 | self.friend_list.parse_groups(self.http_client.get_json( 301 | 'http://s.web2.qq.com/api/get_group_name_list_mask2', 302 | data={'r': json.dumps({ 303 | 'hash': self.get_qq_hash(), 304 | 'vfwebqq': self.vfwebqq})}, 305 | headers=self.default_headers)) 306 | log.i('list', 'Group list fetched.') 307 | 308 | def get_discus_list(self): 309 | self.friend_list.parse_discus(self.http_client.get_json( 310 | 'http://s.web2.qq.com/api/get_discus_list', 311 | data={'clientid': 53999199, 312 | 'psessionid': self.psessionid, 313 | 'vfwebqq': self.vfwebqq, 314 | 't': utime()}, 315 | headers=self.default_headers)) 316 | log.i('list', 'Discus group list fetched.') 317 | 318 | def get_online_buddies(self): 319 | # method is GET 320 | url_get_online = ( 321 | 'http://d1.web2.qq.com/channel/get_online_buddies2?' 322 | 'vfwebqq={}&clientid={}&psessionid={}&t={}').format( 323 | self.vfwebqq, 53999199, self.psessionid, utime()) 324 | self.friend_list.parse_online_buddies( 325 | self.http_client.get_json( 326 | url_get_online, headers=self.poll_headers)) 327 | log.i('list', 'Online buddies list fetched.') 328 | 329 | def get_recent_list(self): 330 | self.friend_list.parse_recent(self.http_client.get_json( 331 | 'http://d1.web2.qq.com/channel/get_recent_list2', 332 | data={'r': json.dumps({ 333 | 'vfwebqq': self.vfwebqq, 334 | 'clientid': 53999199, 335 | 'psessionid': self.psessionid})}, 336 | headers=self.poll_headers)) 337 | log.i('list', 'Recent list fetched.') 338 | 339 | def get_self_info(self): 340 | # method is GET 341 | if not hasattr(self, 'info'): 342 | r = self.http_client.get_json( 343 | 'http://s.web2.qq.com/api/get_self_info2?t' + str(utime()), 344 | headers=self.default_headers) 345 | if r['retcode'] == 0: 346 | self.info = r['result'] 347 | else: 348 | log.e('info', 'User self info fetching failed.') 349 | return self.info 350 | 351 | def get_user_info(self, uin): 352 | # method is GET 353 | r = self.friend_list.get_user_info(uin) 354 | if r is not None: 355 | return r 356 | else: 357 | url_get_user_info = ( 358 | 'http://s.web2.qq.com/api/get_friend_info2?' 359 | 'tuin={}&vfwebqq={}&clientid=53999199&psessionid={}&' 360 | 't={}').format(uin, self.vfwebqq, self.psessionid, utime()) 361 | j = self.http_client.get_json( 362 | url_get_user_info, headers=self.default_headers) 363 | return self.friend_list.parse_user_info(j) 364 | 365 | def get_group_info(self, gid): 366 | # method is GET 367 | r = self.friend_list.get_group_info(gid) 368 | if r is not None: 369 | return r 370 | else: 371 | url_get_group_info = ( 372 | 'http://s.web2.qq.com/api/get_group_info_ext2?' 373 | 'gcode={}&vfwebqq={}&t={}').format( 374 | self.friend_list.g[gid]['code'], self.vfwebqq, utime()) 375 | print(url_get_group_info) 376 | j = self.http_client.get_json( 377 | url_get_group_info, headers=self.default_headers) 378 | return self.friend_list.parse_group_info(j) 379 | 380 | def send_buddy_message(self, uin, content, 381 | font="宋体", size=10, color='000000'): 382 | content = content.replace('<', '<') 383 | content = content.replace('>', '>') 384 | self.msg_id += 1 385 | c = json.dumps([ 386 | content, ["font", 387 | {"name": font, "size": size, 388 | "style": [0, 0, 0], "color": color}]]) 389 | self.http_client.req_async( 390 | 'http://d1.web2.qq.com/channel/send_buddy_msg2', 391 | data={'r': json.dumps({ 392 | 'to': uin, 'content': c, 393 | 'face': self.friend_list.f[uin]['face'], 394 | 'clientid': 53999199, 'msg_id': self.msg_id, 395 | 'psessionid': self.psessionid})}, 396 | headers=self.poll_headers, 397 | cb=self._callback_send) 398 | 399 | def send_group_message(self, gid, content, 400 | font="宋体", size=10, color='000000'): 401 | content = content.replace('<', '<') 402 | content = content.replace('>', '>') 403 | self.msg_id += 1 404 | c = json.dumps([ 405 | content, ["font", 406 | {"name": font, "size": size, 407 | "style": [0, 0, 0], "color": color}]]) 408 | self.http_client.req_async( 409 | 'http://d1.web2.qq.com/channel/send_qun_msg2', 410 | data={'r': json.dumps({ 411 | 'group_uin': gid, 'content': c, 412 | 'face': 0, # TODO figure out what `face` is 413 | 'clientid': 53999199, 'msg_id': self.msg_id, 414 | 'psessionid': self.psessionid})}, 415 | headers=self.poll_headers, 416 | cb=self._callback_send) 417 | 418 | def get_real_uin(self, tuin): 419 | """Get user's real uin by tuin 420 | WebQQ protocol itself uses `tuin` which is not the original uin, 421 | getting the real uin requires an API request, which is exactly 422 | what this method does. 423 | Returns user's real uin. 424 | Client.get_real_uin(tuin) -> int 425 | """ 426 | # method is GET 427 | j = self.http_client.get_json(( 428 | 'http://s.web2.qq.com/api/get_friend_uin2?tuin={}&type=1&' 429 | 'vfwebqq={}&t={}').format(tuin, self.vfwebqq, utime()), 430 | headers = self.default_headers) 431 | if j['retcode'] != 0: 432 | raise RuntimeError('get_real_uin failed: illegal arguments.') 433 | else: 434 | return j['result']['account'] 435 | 436 | def add_handler(self, handler): 437 | handler.set_qq_client(self) 438 | self.handlers.append(handler) 439 | 440 | 441 | class QQHandler(object): 442 | 443 | def __init__(self): 444 | self._qq_client = None 445 | 446 | def set_qq_client(self, c): 447 | if not isinstance(c, QQClient): 448 | raise TypeError('QQHandler: not a QQClient object.') 449 | self._qq_client = c 450 | 451 | def __getattr__(self, name): 452 | if hasattr(self._qq_client, name): 453 | return self._qq_client.__getattribute__(name) 454 | 455 | def on_fail(self, resp, previous): 456 | pass 457 | 458 | def on_buddy_message(self, uin, msg): 459 | pass 460 | 461 | def on_group_message(self, gid, uin, msg): 462 | pass 463 | --------------------------------------------------------------------------------