├── .gitignore
├── PyMicroChat.zip
├── README.md
├── microchat
├── Util.py
├── __init__.py
├── business.py
├── client_tornado.py
├── define.py
├── dll
│ ├── ecdh_x32.dll
│ └── ecdh_x64.dll
├── dns_ip.py
├── ecdh
│ ├── __init__.py
│ ├── ecdh.py
│ ├── openssl.py
│ ├── x64
│ │ └── libeay32.dll
│ └── x86
│ │ └── libeay32.dll
├── interface.py
├── mm.proto
├── mm_pb2.py
├── plugin
│ ├── __init__.py
│ ├── browser.py
│ ├── check_friend.py
│ ├── color_console.py
│ ├── get_win_browser.py
│ ├── handle_appmsg.py
│ ├── logger_wrapper.py
│ ├── plugin.py
│ ├── revoke_joke.py
│ ├── tuling_robot.py
│ └── verify_friend.py
└── version.py
├── requirements.txt
└── run.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 | build/
3 | dist/
4 | docs/_build/
5 | *.egg-info/
6 | *.egg/
7 | cookies.txt
8 | .coverage
9 | .idea/
10 | .cache/
11 | htmlcov/
12 | venv
13 | include/
14 | Lib/
15 | Scripts/
16 | *.log
17 | *.db
18 | *.cfg
19 | pip-selfcheck.json
20 |
--------------------------------------------------------------------------------
/PyMicroChat.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LSilent/PyMicroChat/e2862b3766f52b924d13f7a75000271dfe1e3d76/PyMicroChat.zip
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PyMicroChat
2 |
3 | ## 声明:本项目仅用于通信方面知识的学习交流,请勿用于非法用途.
4 |
5 | ## Python3.3以上的版本通过venv模块原生支持虚拟环境,可以代替Python之前的virtualenv.低版本请用pip安装。
6 |
7 | > git 下载
8 | >> git clone https://github.com/InfiniteTsukuyomi/PyMicroChat.git
9 | >> or git clone git@github.com:InfiniteTsukuyomi/PyMicroChat.git
10 |
11 | Windows环境
12 | > 创建Python3的venv虚拟环境
13 | >> D:\VSCODE\PyMicroChat>python3 -m venv .
14 |
15 | > 进入虚拟环境
16 | >> D:\VSCODE\PyMicroChat>.\Scripts\activate
17 |
18 | > 首次执行,配置依赖包,以后根据更新依赖环境运行
19 | >> (PyMicroChat) D:\VSCODE\PyMicroChat>pip install -r requirements.txt
20 |
21 | > 修改run.py相应的usrname & passwd,执行
22 | >> (PyMicroChat) D:\VSCODE\PyMicroChat>python run.py
23 |
24 | > 退出
25 | >> (PyMicroChat) D:\VSCODE\PyMicroChat>deactivate
26 |
27 | ### 工程中的dll文件源码地址:https://github.com/InfiniteTsukuyomi/MicroChat/tree/master/test/ecdh
28 |
29 | ##### QQ交流群:306804262
30 |
--------------------------------------------------------------------------------
/microchat/Util.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import http.client
3 | import time
4 | import hashlib
5 | import zlib
6 | import struct
7 | import os
8 | import sys
9 | import webbrowser
10 | import ctypes
11 | import platform
12 | import subprocess
13 | import sqlite3
14 | from . import define
15 | from . import dns_ip
16 | from Crypto.Cipher import AES, DES3
17 | from Crypto.PublicKey import RSA
18 | from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5
19 | from google.protobuf.internal import decoder, encoder
20 | from ctypes import *
21 | from .plugin.logger_wrapper import logger
22 | from .ecdh import ecdh
23 |
24 | ################################全局变量################################
25 | # cgi http头
26 | headers = {
27 | "Accept": "*/*",
28 | "Cache-Control": "no-cache",
29 | "Connection": "close",
30 | "Content-type": "application/octet-stream",
31 | "User-Agent": "MicroMessenger Client"
32 | }
33 |
34 | # 好友类型
35 | CONTACT_TYPE_ALL = 0xFFFF # 所有好友
36 | CONTACT_TYPE_FRIEND = 1 # 朋友
37 | CONTACT_TYPE_CHATROOM = 2 # 群聊
38 | CONTACT_TYPE_OFFICAL = 4 # 公众号
39 | CONTACT_TYPE_BLACKLIST = 8 # 黑名单中的好友
40 | CONTACT_TYPE_DELETED = 16 # 已删除的好友
41 |
42 | # ECDH key
43 | EcdhPriKey = b''
44 | EcdhPubKey = b''
45 |
46 | # session key(封包解密时的aes key/iv)
47 | sessionKey = b''
48 |
49 | # cookie(登陆成功后返回,通常长15字节)
50 | cookie = b''
51 |
52 | # uin
53 | uin = 0
54 |
55 | # wxid
56 | wxid = ''
57 |
58 | # sqlite3数据库
59 | conn = None
60 |
61 |
62 | ########################################################################
63 |
64 | # md5
65 | def GetMd5(src):
66 | m1 = hashlib.md5()
67 | m1.update(src.encode('utf-8'))
68 | return m1.hexdigest()
69 |
70 |
71 | # padding
72 | def pad(s): return s + bytes([16 - len(s) % 16] * (16 - len(s) % 16))
73 | def unpad(s): return s[0:(len(s) - s[-1])]
74 |
75 | # 先压缩后AES-128-CBC加密
76 | def compress_and_aes(src, key):
77 | compressData = zlib.compress(src)
78 | aes_obj = AES.new(key, AES.MODE_CBC, key) # IV与key相同
79 | encrypt_buf = aes_obj.encrypt(pad(compressData))
80 | return (encrypt_buf, len(compressData)) # 需要返回压缩后protobuf长度,组包时使用
81 |
82 | # 不压缩AES-128-CBC加密
83 | def aes(src, key):
84 | aes_obj = AES.new(key, AES.MODE_CBC, key) # IV与key相同
85 | encrypt_buf = aes_obj.encrypt(pad(src))
86 | return encrypt_buf
87 |
88 | # 先压缩后RSA加密
89 | def compress_and_rsa(src):
90 | compressData = zlib.compress(src)
91 | rsakey = RSA.construct((int(define.__LOGIN_RSA_VER158_KEY_N__, 16), define.__LOGIN_RSA_VER158_KEY_E__))
92 | cipher = Cipher_pkcs1_v1_5.new(rsakey)
93 | encrypt_buf = cipher.encrypt(compressData)
94 | return encrypt_buf
95 |
96 | # 不压缩RSA2048加密
97 | def rsa(src):
98 | rsakey = RSA.construct((int(define.__LOGIN_RSA_VER158_KEY_N__, 16), define.__LOGIN_RSA_VER158_KEY_E__))
99 | cipher = Cipher_pkcs1_v1_5.new(rsakey)
100 | encrypt_buf = cipher.encrypt(src)
101 | return encrypt_buf
102 |
103 | # AES-128-CBC解密解压缩
104 | def decompress_and_aesDecrypt(src, key):
105 | aes_obj = AES.new(key, AES.MODE_CBC, key) # IV与key相同
106 | decrypt_buf = aes_obj.decrypt(src)
107 | return zlib.decompress(unpad(decrypt_buf))
108 |
109 | # AES-128-CBC解密
110 | def aesDecrypt(src, key):
111 | aes_obj = AES.new(key, AES.MODE_CBC, key) # IV与key相同
112 | decrypt_buf = aes_obj.decrypt(src)
113 | return unpad(decrypt_buf)
114 |
115 | # HTTP短链接发包
116 | def mmPost(cgi, data):
117 | ret = False
118 | retry = 3
119 | response = b''
120 | while not ret and retry > 0:
121 | retry = retry - 1
122 | try:
123 | conn = http.client.HTTPConnection(dns_ip.fetch_shortlink_ip(), timeout=3)
124 | conn.request("POST", cgi, data, headers)
125 | response = conn.getresponse().read()
126 | conn.close()
127 | ret = True
128 | except Exception as e:
129 | if 'timed out' == str(e):
130 | logger.info('{}请求超时,正在尝试重新发起请求,剩余尝试次数{}次'.format(cgi, retry), 11)
131 | else:
132 | raise RuntimeError('mmPost Error!!!')
133 | return response
134 |
135 | # HTTP短链接发包
136 | def post(host, api, data, head=''):
137 | try:
138 | conn = http.client.HTTPConnection(host, timeout=2)
139 | if head:
140 | conn.request("POST", api, data, head)
141 | else:
142 | conn.request("POST", api, data)
143 | response = conn.getresponse().read()
144 | conn.close()
145 | return response
146 | except:
147 | logger.info('{}请求失败!'.format(api) ,11)
148 | return b''
149 |
150 |
151 | # 退出程序
152 | def ExitProcess():
153 | logger.info('===========bye===========')
154 | os.system("pause")
155 | sys.exit()
156 |
157 | # 使用IE浏览器访问网页(阻塞)
158 | def OpenIE(url):
159 | subprocess.call('python ./microchat/plugin/browser.py {}'.format(url))
160 |
161 | # 使用c接口生成ECDH本地密钥对
162 | def GenEcdhKey(old=False):
163 | global EcdhPriKey, EcdhPubKey
164 | if old:
165 | # 载入c模块
166 | loader = ctypes.cdll.LoadLibrary
167 | if platform.architecture()[0] == '64bit':
168 | lib = loader("./microchat/dll/ecdh_x64.dll")
169 | else:
170 | lib = loader("./microchat/dll/ecdh_x32.dll")
171 | # 申请内存
172 | priKey = bytes(bytearray(2048)) # 存放本地DH私钥
173 | pubKey = bytes(bytearray(2048)) # 存放本地DH公钥
174 | lenPri = c_int(0) # 存放本地DH私钥长度
175 | lenPub = c_int(0) # 存放本地DH公钥长度
176 | # 转成c指针传参
177 | pri = c_char_p(priKey)
178 | pub = c_char_p(pubKey)
179 | pLenPri = pointer(lenPri)
180 | pLenPub = pointer(lenPub)
181 | # secp224r1 ECC算法
182 | nid = 713
183 | # c函数原型:bool GenEcdh(int nid, unsigned char *szPriKey, int *pLenPri, unsigned char *szPubKey, int *pLenPub);
184 | bRet = lib.GenEcdh(nid, pri, pLenPri, pub, pLenPub)
185 | if bRet:
186 | # 从c指针取结果
187 | EcdhPriKey = priKey[:lenPri.value]
188 | EcdhPubKey = pubKey[:lenPub.value]
189 | return bRet
190 | else:
191 | try:
192 | pub_key, len_pub, pri_key, len_pri = ecdh.gen_ecdh(713)
193 | EcdhPubKey = pub_key[:len_pub]
194 | EcdhPriKey = pri_key[:len_pri]
195 | return True
196 | except Exception as e:
197 | print(e)
198 | return False
199 |
200 | # 密钥协商
201 | def DoEcdh(serverEcdhPubKey, old=False):
202 | if old:
203 | EcdhShareKey = b''
204 | # 载入c模块
205 | loader = ctypes.cdll.LoadLibrary
206 | if platform.architecture()[0] == '64bit':
207 | lib = loader("./microchat/dll/ecdh_x64.dll")
208 | else:
209 | lib = loader("./microchat/dll/ecdh_x32.dll") #修改在32环境下找不到ecdh_x32.dll文件 by luckyfish 2018-04-02
210 | # 申请内存
211 | shareKey = bytes(bytearray(2048)) # 存放密钥协商结果
212 | lenShareKey = c_int(0) # 存放共享密钥长度
213 | # 转成c指针传参
214 | pShareKey = c_char_p(shareKey)
215 | pLenShareKey = pointer(lenShareKey)
216 | pri = c_char_p(EcdhPriKey)
217 | pub = c_char_p(serverEcdhPubKey)
218 | # secp224r1 ECC算法
219 | nid = 713
220 | # c函数原型:bool DoEcdh(int nid, unsigned char * szServerPubKey, int nLenServerPub, unsigned char * szLocalPriKey, int nLenLocalPri, unsigned char * szShareKey, int *pLenShareKey);
221 | bRet = lib.DoEcdh(nid, pub, len(serverEcdhPubKey), pri,len(EcdhPriKey), pShareKey, pLenShareKey)
222 | if bRet:
223 | # 从c指针取结果
224 | EcdhShareKey = shareKey[:lenShareKey.value]
225 | return EcdhShareKey
226 | else:
227 | try:
228 | _, EcdhShareKey = ecdh.do_ecdh(713, serverEcdhPubKey, EcdhPriKey)
229 | return EcdhShareKey
230 | except Exception as e:
231 | print(e)
232 | return b''
233 |
234 |
235 | # bytes转hex输出
236 | def b2hex(s): return ''.join(["%02X " % x for x in s]).strip()
237 |
238 | # sqlite3数据库初始化
239 | def init_db():
240 | global conn
241 | # 建db文件夹
242 | if not os.path.exists(os.getcwd() + '/db'):
243 | os.mkdir(os.getcwd() + '/db')
244 | # 建库
245 | conn = sqlite3.connect('./db/mm_{}.db'.format(wxid))
246 | cur = conn.cursor()
247 | # 建消息表
248 | cur.execute('create table if not exists msg(svrid bigint unique,clientMsgId varchar(1024),utc integer,createtime varchar(1024),fromWxid varchar(1024),toWxid varchar(1024),type integer,content text(65535))')
249 | # 建联系人表
250 | cur.execute('create table if not exists contact(wxid varchar(1024) unique,nick_name varchar(1024),remark_name varchar(1024),alias varchar(1024),avatar_big varchar(1024),v1_name varchar(1024),type integer default(0),sex integer,country varchar(1024),sheng varchar(1024),shi varchar(1024),qianming varchar(2048),register_body varchar(1024),src integer,chatroom_owner varchar(1024),chatroom_serverVer integer,chatroom_max_member integer,chatroom_member_cnt integer)')
251 | # 建sync key表
252 | cur.execute('create table if not exists synckey(key varchar(4096))')
253 | return
254 |
255 | # 获取sync key
256 | def get_sync_key():
257 | cur = conn.cursor()
258 | cur.execute('select * from synckey')
259 | row = cur.fetchone()
260 | if row:
261 | return bytes.fromhex(row[0])
262 | return b''
263 |
264 | # 刷新sync key
265 | def set_sync_key(key):
266 | cur = conn.cursor()
267 | cur.execute('delete from synckey')
268 | cur.execute('insert into synckey(key) values("{}")'.format(b2hex(key)))
269 | conn.commit()
270 | return
271 |
272 | # 保存消息
273 | def insert_msg_to_db(svrid, utc, from_wxid, to_wxid, type, content, client_msg_id = ''):
274 | cur = conn.cursor()
275 | try:
276 | cur.execute("insert into msg(svrid, clientMsgId, utc, createtime, fromWxid, toWxid, type, content) values('{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}')".format(
277 | svrid, client_msg_id, utc, utc_to_local_time(utc), from_wxid, to_wxid, type, content))
278 | conn.commit()
279 | except Exception as e:
280 | logger.info('insert_msg_to_db error:{}'.format(str(e)))
281 | return
282 |
283 | # 保存/刷新好友消息
284 | def insert_contact_info_to_db(wxid, nick_name, remark_name, alias, avatar_big, v1_name, type, sex, country, sheng, shi, qianming, register_body, src, chatroom_owner, chatroom_serverVer, chatroom_max_member, chatroom_member_cnt):
285 | cur = conn.cursor()
286 | try:
287 | # 先删除旧的信息
288 | cur.execute("delete from contact where wxid = '{}'".format(wxid))
289 | # 插入最新联系人数据
290 | cur.execute("insert into contact(wxid,nick_name,remark_name,alias,avatar_big,v1_name,type,sex,country,sheng,shi,qianming,register_body,src,chatroom_owner,chatroom_serverVer,chatroom_max_member,chatroom_member_cnt) values('{}','{}','{}','{}','{}','{}','{}','{}','{}','{}','{}','{}','{}','{}','{}','{}','{}','{}')".format(
291 | wxid, nick_name, remark_name, alias, avatar_big, v1_name, type, sex, country, sheng, shi, qianming, register_body, src, chatroom_owner, chatroom_serverVer, chatroom_max_member, chatroom_member_cnt))
292 | conn.commit()
293 | except Exception as e:
294 | logger.info('insert_contact_info_to_db error:{}'.format(str(e)))
295 | return
296 |
297 | # utc转本地时间
298 | def utc_to_local_time(utc):
299 | return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(utc))
300 |
301 | # 获取本地时间
302 | def get_utc():
303 | return int(time.time())
304 |
305 | # str转bytes
306 | def str2bytes(s): return bytes(s, encoding="utf8")
307 |
308 | # 获取添加好友方式
309 | def get_way(src):
310 | if src in define.WAY.keys() or (src - 1000000) in define.WAY.keys():
311 | if src > 1000000:
312 | return '对方通过' + define.WAY[src-1000000] + '添加'
313 | elif src:
314 | return '通过' + define.WAY[src] + '添加'
315 | return define.WAY[0]
316 |
317 | # 获取好友类型:
318 | def get_frient_type(wxid):
319 | cur = conn.cursor()
320 | cur.execute("select type from contact where wxid = '{}'".format(wxid))
321 | row = cur.fetchone()
322 | if row:
323 | return row[0]
324 | else:
325 | return 0
326 |
327 | # 是否在我的好友列表中
328 | def is_in_my_friend_list(type):
329 | # type的最后一bit是1表示对方在我的好友列表中
330 | return (type & 1)
331 |
332 | # 对方是否在我的黑名单中
333 | def is_in_blacklist(type):
334 | return (type & (1 << 3))
335 |
336 | # 获取好友列表wxid,昵称,备注,alias,v1_name,头像
337 | def get_contact(contact_type):
338 | cur = conn.cursor()
339 | rows = []
340 | if contact_type & CONTACT_TYPE_FRIEND: # 返回好友列表
341 | cur.execute("select wxid,nick_name,remark_name,alias,v1_name,avatar_big from contact where wxid not like '%%@chatroom' and wxid not like 'gh_%%' and (type & 8) = 0 and (type & 1)")
342 | rows = rows + cur.fetchall()
343 | if contact_type & CONTACT_TYPE_CHATROOM: # 返回群聊列表
344 | cur.execute("select wxid,nick_name,remark_name,alias,v1_name,avatar_big from contact where wxid like '%%@chatroom' and (type & 1)")
345 | rows = rows + cur.fetchall()
346 | if contact_type & CONTACT_TYPE_OFFICAL: # 返回公众号列表
347 | cur.execute("select wxid,nick_name,remark_name,alias,v1_name,avatar_big from contact where wxid like 'gh_%%' and (type & 1)")
348 | rows = rows + cur.fetchall()
349 | if contact_type & CONTACT_TYPE_BLACKLIST: # 返回黑名单列表
350 | cur.execute("select wxid,nick_name,remark_name,alias,v1_name,avatar_big from contact where wxid not like '%%@chatroom' and wxid not like 'gh_%%' and (type & 8)")
351 | rows = rows + cur.fetchall()
352 | if contact_type & CONTACT_TYPE_DELETED: # 返回已删除好友列表(非主动删除对方?)
353 | cur.execute("select wxid,nick_name,remark_name,alias,v1_name,avatar_big from contact where wxid not like '%%@chatroom' and wxid not like 'gh_%%' and (type & 1) = 0")
354 | rows = rows + cur.fetchall()
355 | return rows
356 |
357 | # 截取字符串
358 | def find_str(src,start,end):
359 | try:
360 | if src.find(start)> -1:
361 | start_index = src.find(start) + len(start)
362 | if end and src.find(start)> -1:
363 | end_index = src.find(end,start_index)
364 | return src[start_index:end_index]
365 | else:
366 | return src[start_index:]
367 | except:
368 | pass
369 | return ''
370 |
371 | # 转账相关请求参数签名算法
372 | def SignWith3Des(src):
373 | # 首先对字符串取MD5
374 | raw_buf = hashlib.md5(src.encode()).digest()
375 | # 对16字节的md5做bcd2ascii
376 | bcd_to_ascii = bytearray(32)
377 | for i in range(len(raw_buf)):
378 | bcd_to_ascii[2*i] = raw_buf[i]>>4
379 | bcd_to_ascii[2*i+1] = raw_buf[i] & 0xf
380 | # hex_to_bin转换加密key
381 | key = bytes.fromhex('3ECA2F6FFA6D4952ABBACA5A7B067D23')
382 | # 对32字节的bcd_to_ascii做Triple DES加密(3DES/ECB/NoPadding)
383 | des3 = DES3.new(key, DES3.MODE_ECB)
384 | enc_bytes = des3.encrypt(bcd_to_ascii)
385 | # bin_to_hex得到最终加密结果
386 | enc_buf = ''.join(["%02X" % x for x in enc_bytes]).strip()
387 | return enc_buf
388 |
389 | # 根据svrid查询client_msg_id
390 | def get_client_msg_id(svrid):
391 | try:
392 | cur = conn.cursor()
393 | cur.execute("select clientMsgId from msg where svrid = '{}'".format(svrid))
394 | row = cur.fetchone()
395 | if row:
396 | return row[0]
397 | except:
398 | pass
399 | return ''
--------------------------------------------------------------------------------
/microchat/__init__.py:
--------------------------------------------------------------------------------
1 | from .version import __version__
2 | from .plugin.color_console import ColorConsole
3 |
4 | def logo_bingo():
5 | print("")
6 | print(ColorConsole.rainbow('****************************************************************************************'))
7 | print(ColorConsole.rainbow('* *'))
8 | print(ColorConsole.rainbow('* ______ __ ____ ____ _____ ____ _ _ __ _______ *'))
9 | print(ColorConsole.rainbow('* / \ | | | __| | \ | _ | | __| | |__| | / \ |__ __| *'))
10 | print(ColorConsole.rainbow('* / /\ /\ \ | | | |__ | / | |_| | | |__ | __ | / /\ \ | | *'))
11 | print(ColorConsole.rainbow('* /_/ \/ \_\ |__| |____| |_/\_\ |_____| |____| |_| |_| /_/ \_\ |_| *'))
12 | print(ColorConsole.rainbow('* *'))
13 | print(ColorConsole.rainbow('* *'))
14 | print(ColorConsole.rainbow('****************************************************************************************'))
15 | print("\n")
16 |
--------------------------------------------------------------------------------
/microchat/business.py:
--------------------------------------------------------------------------------
1 | import http.client
2 | import logging
3 | import random
4 | import struct
5 | import string
6 | import time
7 | import urllib
8 | import xmltodict
9 | import zlib
10 | from . import define
11 | from . import dns_ip
12 | from . import mm_pb2
13 | from .plugin import plugin
14 | from . import Util
15 | from .plugin.logger_wrapper import logger
16 | from google.protobuf.internal import decoder, encoder
17 |
18 | # 组包(压缩加密+封包),参数:protobuf序列化后数据,cgi类型,是否使用压缩算法
19 | def pack(src, cgi_type, use_compress=0):
20 | # 必要参数合法性判定
21 | if not Util.cookie or not Util.uin or not Util.sessionKey:
22 | return b''
23 | # 压缩加密
24 | len_proto_compressed = len(src)
25 | if use_compress:
26 | (body, len_proto_compressed) = Util.compress_and_aes(src, Util.sessionKey)
27 | else:
28 | body = Util.aes(src, Util.sessionKey)
29 | logger.debug("cgi:{},protobuf数据:{}\n加密后数据:{}".format(cgi_type, Util.b2hex(src), Util.b2hex(body)))
30 | # 封包包头
31 | header = bytearray(0)
32 | header += b'\xbf' # 标志位(可忽略该字节)
33 | header += bytes([0]) # 最后2bit:02--包体不使用压缩算法;前6bit:包头长度,最后计算
34 | header += bytes([((0x5 << 4) + 0xf)]) # 05:AES加密算法 0xf:cookie长度(默认使用15字节长的cookie)
35 | header += struct.pack(">I", define.__CLIENT_VERSION__) # 客户端版本号 网络字节序
36 | header += struct.pack(">i", Util.uin) # uin
37 | header += Util.cookie # cookie
38 | header += encoder._VarintBytes(cgi_type) # cgi type
39 | header += encoder._VarintBytes(len(src)) # body proto压缩前长度
40 | header += encoder._VarintBytes(len_proto_compressed) # body proto压缩后长度
41 | header += bytes([0]*15) # 3个未知变长整数参数,共15字节
42 | header[1] = (len(header) << 2) + (1 if use_compress else 2) # 包头长度
43 | logger.debug("包头数据:{}".format(Util.b2hex(header)))
44 | # 组包
45 | senddata = header + body
46 | return senddata
47 |
48 | # 解包
49 | def UnPack(src, key=b''):
50 | if len(src) < 0x20:
51 | raise RuntimeError('Unpack Error!Please check mm protocol!') # 协议需要更新
52 | return b''
53 | if not key:
54 | key = Util.sessionKey
55 | # 解析包头
56 | nCur = 0
57 | if src[nCur] == struct.unpack('>B', b'\xbf')[0]:
58 | nCur += 1 # 跳过协议标志位
59 | nLenHeader = src[nCur] >> 2 # 包头长度
60 | bUseCompressed = (src[nCur] & 0x3 == 1) # 包体是否使用压缩算法:01使用,02不使用
61 | nCur += 1
62 | nDecryptType = src[nCur] >> 4 # 解密算法(固定为AES解密): 05 aes解密 / 07 rsa解密
63 | nLenCookie = src[nCur] & 0xf # cookie长度
64 | nCur += 1
65 | nCur += 4 # 服务器版本(当前固定返回4字节0)
66 | uin = struct.unpack('>i', src[nCur:nCur+4])[0] # uin
67 | nCur += 4
68 | cookie_temp = src[nCur:nCur+nLenCookie] # cookie
69 | if cookie_temp and not(cookie_temp == Util.cookie):
70 | Util.cookie = cookie_temp # 刷新cookie
71 | nCur += nLenCookie
72 | (nCgi, nCur) = decoder._DecodeVarint(src, nCur) # cgi type
73 | (nLenProtobuf, nCur) = decoder._DecodeVarint(src, nCur) # 压缩前protobuf长度
74 | (nLenCompressed, nCur) = decoder._DecodeVarint(src, nCur) # 压缩后protobuf长度
75 | logger.debug('包头长度:{}\n是否使用压缩算法:{}\n解密算法:{}\ncookie长度:{}\nuin:{}\ncookie:{}\ncgi type:{}\nprotobuf长度:{}\n压缩后protobuf长度:{}'.format(
76 | nLenHeader, bUseCompressed, nDecryptType, nLenCookie, uin, str(Util.cookie), nCgi, nLenProtobuf, nLenCompressed))
77 | # 对包体aes解密解压缩
78 | body = src[nLenHeader:] # 取包体数据
79 | if bUseCompressed:
80 | protobufData = Util.decompress_and_aesDecrypt(body, key)
81 | else:
82 | protobufData = Util.aesDecrypt(body, key)
83 | logger.debug('解密后数据:%s' % str(protobufData))
84 | return protobufData
85 |
86 | # 登录组包函数
87 | def login_req2buf(name, password):
88 | # 随机生成16位登录包AesKey
89 | login_aes_key = bytes(''.join(random.sample(string.ascii_letters + string.digits, 16)), encoding="utf8")
90 |
91 | # protobuf组包1
92 | accountRequest = mm_pb2.ManualAuthAccountRequest(
93 | aes=mm_pb2.ManualAuthAccountRequest.AesKey(
94 | len=16,
95 | key=login_aes_key
96 | ),
97 | ecdh=mm_pb2.ManualAuthAccountRequest.Ecdh(
98 | nid=713,
99 | ecdhKey=mm_pb2.ManualAuthAccountRequest.Ecdh.EcdhKey(
100 | len=len(Util.EcdhPubKey),
101 | key=Util.EcdhPubKey
102 | )
103 | ),
104 | userName=name,
105 | password1=Util.GetMd5(password),
106 | password2=Util.GetMd5(password)
107 | )
108 | # protobuf组包2
109 | deviceRequest = mm_pb2.ManualAuthDeviceRequest(
110 | login=mm_pb2.LoginInfo(
111 | aesKey=login_aes_key,
112 | uin=0,
113 | guid=define.__GUID__ + '\0', # guid以\0结尾
114 | clientVer=define.__CLIENT_VERSION__,
115 | androidVer=define.__ANDROID_VER__,
116 | unknown=1,
117 | ),
118 | tag2=mm_pb2.ManualAuthDeviceRequest._Tag2(),
119 | imei=define.__IMEI__,
120 | softInfoXml=define.__SOFTINFO__.format(define.__IMEI__, define.__ANDROID_ID__, define.__MANUFACTURER__+" "+define.__MODELNAME__, define.__MOBILE_WIFI_MAC_ADDRESS__,
121 | define.__CLIENT_SEQID_SIGN__, define.__AP_BSSID__, define.__MANUFACTURER__, "taurus", define.__MODELNAME__, define.__IMEI__),
122 | unknown5=0,
123 | clientSeqID=define.__CLIENT_SEQID__,
124 | clientSeqID_sign=define.__CLIENT_SEQID_SIGN__,
125 | loginDeviceName=define.__MANUFACTURER__+" "+define.__MODELNAME__,
126 | deviceInfoXml=define.__DEVICEINFO__.format(
127 | define.__MANUFACTURER__, define.__MODELNAME__),
128 | language=define.__LANGUAGE__,
129 | timeZone="8.00",
130 | unknown13=0,
131 | unknown14=0,
132 | deviceBrand=define.__MANUFACTURER__,
133 | deviceModel=define.__MODELNAME__+"armeabi-v7a",
134 | osType=define.__ANDROID_VER__,
135 | realCountry="cn",
136 | unknown22=2, # Unknown
137 | )
138 |
139 | logger.debug("accountData protobuf数据:" + Util.b2hex(accountRequest.SerializeToString()))
140 | logger.debug("deviceData protobuf数据:" + Util.b2hex(deviceRequest.SerializeToString()))
141 |
142 | # 加密
143 | reqAccount = Util.compress_and_rsa(accountRequest.SerializeToString())
144 | reqDevice = Util.compress_and_aes(deviceRequest.SerializeToString(), login_aes_key)
145 |
146 | logger.debug("加密后数据长度:reqAccount={},reqDevice={}".format(len(reqAccount), len(reqDevice[0])))
147 | logger.debug("加密后reqAccount数据:" + Util.b2hex(reqAccount))
148 | logger.debug("加密后reqDevice数据:" + Util.b2hex(reqDevice[0]))
149 |
150 | # 封包包体
151 | subheader = b''
152 | subheader += struct.pack(">I", len(accountRequest.SerializeToString())) # accountData protobuf长度
153 | subheader += struct.pack(">I", len(deviceRequest.SerializeToString())) # deviceData protobuf长度
154 | subheader += struct.pack(">I", len(reqAccount)) # accountData RSA加密后长度
155 | # 包体由头信息、账号密码加密后数据、硬件设备信息加密后数据3部分组成
156 | body = subheader + reqAccount + reqDevice[0]
157 |
158 | # 封包包头
159 | header = bytearray(0)
160 | header += bytes([0]) # 最后2bit:02--包体不使用压缩算法;前6bit:包头长度,最后计算
161 | header += bytes([((0x7 << 4) + 0xf)]) # 07:RSA加密算法 0xf:cookie长度
162 | header += struct.pack(">I", define.__CLIENT_VERSION__) # 客户端版本号 网络字节序
163 | header += bytes([0]*4) # uin
164 | header += bytes([0]*15) # coockie
165 | header += encoder._VarintBytes(701) # cgi type
166 | header += encoder._VarintBytes(len(body)) # body 压缩前长度
167 | header += encoder._VarintBytes(len(body)) # body 压缩后长度(登录包不需要压缩body数据)
168 | header += struct.pack(">B", define.__LOGIN_RSA_VER__) # RSA秘钥版本
169 | header += b'\x01\x02' # Unknown Param
170 | header[0] = (len(header) << 2) + 2 # 包头长度
171 |
172 | # 组包
173 | logger.debug('包体数据:' + str(body))
174 | logger.debug('包头数据:' + str(header))
175 | senddata = header + body
176 |
177 | return (senddata, login_aes_key)
178 |
179 | # 登录解包函数
180 | def login_buf2Resp(buf, login_aes_key):
181 | # 解包
182 | loginRes = mm_pb2.ManualAuthResponse()
183 | loginRes.result.code = -1
184 | loginRes.ParseFromString(UnPack(buf, login_aes_key))
185 |
186 | # 更新DNS信息
187 | for dns_info in loginRes.dns.redirect.real_host:
188 | if dns_info.host == 'long.weixin.qq.com': # 更新长链接信息
189 | dns_ip.long_ip = [] # 清空旧的长链接ip池
190 | for ip_info in loginRes.dns.ip.longlink:
191 | if dns_info.redirect == ip_info.host.replace('\x00', ''):
192 | logger.debug('更新长链接DNS:[{}:{}]'.format(dns_info.redirect, ip_info.ip.replace('\x00', '')))
193 | dns_ip.long_ip.append(ip_info.ip.replace('\x00', '')) # 保存长链接ip
194 | elif dns_info.host == 'short.weixin.qq.com': # 更新短链接信息
195 | dns_ip.short_ip = [] # 清空旧的短链接ip池
196 | for ip_info in loginRes.dns.ip.shortlink:
197 | if dns_info.redirect == ip_info.host.replace('\x00', ''):
198 | logger.debug('更新短链接DNS:[{}:{}]'.format(dns_info.redirect, ip_info.ip.replace('\x00', '')))
199 | dns_ip.short_ip.append(ip_info.ip.replace('\x00', '')) # 保存短链接ip
200 | # 保存dns到db
201 | dns_ip.save_dns()
202 |
203 | # 登录异常处理
204 | if -301 == loginRes.result.code: # DNS解析失败,请尝试更换idc
205 | logger.error('登陆结果:\ncode:{}\n即将尝试切换DNS重新登陆!'.format(loginRes.result.code))
206 | elif -106 == loginRes.result.code: # 需要在IE浏览器中滑动操作解除环境异常/扫码、短信、好友授权(滑动解除异常后需要重新登录一次)
207 | logger.error('登陆结果:\ncode:{}\nError msg:{}\n'.format(loginRes.result.code, loginRes.result.err_msg.msg[loginRes.result.err_msg.msg.find('')]))
208 | # 打开IE,完成授权
209 | logger.error('请在浏览器授权后重新登陆!')
210 | Util.OpenIE(loginRes.result.err_msg.msg[loginRes.result.err_msg.msg.find('')])
211 | elif loginRes.result.code: # 其他登录错误
212 | logger.error('登陆结果:\ncode:{}\nError msg:{}\n'.format(loginRes.result.code, loginRes.result.err_msg.msg[loginRes.result.err_msg.msg.find(
213 | '')]))
214 | else: # 登陆成功
215 | # 密钥协商
216 | Util.sessionKey = Util.aesDecrypt(loginRes.authParam.session.key, Util.DoEcdh(loginRes.authParam.ecdh.ecdhKey.key))
217 | # 保存uin/wxid
218 | Util.uin = loginRes.authParam.uin
219 | Util.wxid = loginRes.accountInfo.wxId
220 | logger.info('登陆成功!\nsession_key:{}\nuin:{}\nwxid:{}\nnickName:{}\nalias:{}'.format(Util.sessionKey, Util.uin, Util.wxid, loginRes.accountInfo.nickName, loginRes.accountInfo.Alias),13)
221 | # 初始化db
222 | Util.init_db()
223 |
224 | return loginRes.result.code
225 |
226 | # 首次登录设备初始化组包函数
227 | def new_init_req2buf(cur=b'', max=b''):
228 | # protobuf组包
229 | new_init_request = mm_pb2.NewInitRequest(
230 | login=mm_pb2.LoginInfo(
231 | aesKey=Util.sessionKey,
232 | uin=Util.uin,
233 | guid=define.__GUID__ + '\0', # guid以\0结尾
234 | clientVer=define.__CLIENT_VERSION__,
235 | androidVer=define.__ANDROID_VER__,
236 | unknown=3,
237 | ),
238 | wxid=Util.wxid,
239 | sync_key_cur=cur,
240 | sync_key_max=max,
241 | language=define.__LANGUAGE__,
242 | )
243 |
244 | # 组包
245 | return pack(new_init_request.SerializeToString(), 139)
246 |
247 | # 更新好友信息
248 | def update_contact_info(friend,update = True):
249 | tag = '[更新好友信息]' if update else '[查询好友信息]'
250 | is_friend = '<好友>' if (friend.type & 1) else '<非好友>'
251 | # 过滤系统wxid
252 | if friend.wxid.id in define.MM_DEFAULT_WXID:
253 | logger.info('{}:跳过默认wxid[{}]'.format(tag, friend.wxid.id), 6)
254 | return
255 | # 好友分类
256 | if friend.wxid.id.endswith('@chatroom'): # 群聊
257 | logger.info('{}{}:群聊名:{} 群聊wxid:{} chatroom_serverVer:{} chatroom_max_member:{} 群主:{} 群成员数量:{}'.format(tag, is_friend, friend.nickname.name, friend.wxid.id, friend.chatroom_serverVer, friend.chatroom_max_member, friend.chatroomOwnerWxid, friend.group_member_list.cnt), 6)
258 | elif friend.wxid.id.startswith('gh_'): # 公众号
259 | logger.info('{}{}:公众号:{} 公众号wxid:{} alias:{} 注册主体:{}'.format(tag, is_friend, friend.nickname.name, friend.wxid.id, friend.alias, friend.register_body if friend.register_body_type == 24 else '个人'), 6)
260 | else: # 好友
261 | logger.info('{}{}:昵称:{} 备注名:{} wxid:{} alias:{} 性别:{} 好友来源:{} 个性签名:{}'.format(tag, is_friend, friend.nickname.name, friend.remark_name.name, friend.wxid.id, friend.alias, friend.sex, Util.get_way(friend.src), friend.qianming), 6)
262 | if update or friend.wxid.id.endswith('@chatroom'):
263 | # 将好友信息存入数据库
264 | Util.insert_contact_info_to_db(friend.wxid.id, friend.nickname.name, friend.remark_name.name, friend.alias, friend.avatar_big, friend.v1_name, friend.type, friend.sex, friend.country,friend.sheng, friend.shi, friend.qianming, friend.register_body, friend.src, friend.chatroomOwnerWxid, friend.chatroom_serverVer, friend.chatroom_max_member, friend.group_member_list.cnt)
265 | return
266 |
267 | # 首次登录设备初始化解包函数
268 | def new_init_buf2resp(buf):
269 | # 解包
270 | res = mm_pb2.NewInitResponse()
271 | res.ParseFromString(UnPack(buf))
272 |
273 | # newinit后保存sync key
274 | Util.set_sync_key(res.sync_key_cur) # newinit结束前不要异步调用newsync
275 | logger.debug('newinit sync_key_cur len:{}\ndata:{}'.format(len(res.sync_key_cur), Util.b2hex(res.sync_key_cur)))
276 | logger.debug('newinit sync_key_max len:{}\ndata:{}'.format(len(res.sync_key_max), Util.b2hex(res.sync_key_max)))
277 |
278 | # 初始化数据
279 | logger.info('newinit cmd数量:{},是否需要继续初始化:{}'.format(res.cntList, res.continue_flag))
280 |
281 | # 初始化
282 | for i in range(res.cntList):
283 | if 5 == res.tag7[i].type: # 未读消息
284 | msg = mm_pb2.Msg()
285 | msg.ParseFromString(res.tag7[i].data.data)
286 | if 10002 == msg.type or 9999 == msg.type: # 过滤系统垃圾消息
287 | continue
288 | else:
289 | # 将消息存入数据库
290 | Util.insert_msg_to_db(msg.serverid, msg.createTime, msg.from_id.id, msg.to_id.id, msg.type, msg.raw.content)
291 | logger.info('收到新消息:\ncreate utc time:{}\ntype:{}\nfrom:{}\nto:{}\nraw data:{}\nxml data:{}'.format(Util.utc_to_local_time(msg.createTime), msg.type, msg.from_id.id, msg.to_id.id, msg.raw.content, msg.xmlContent))
292 | elif 2 == res.tag7[i].type: # 好友列表
293 | friend = mm_pb2.contact_info()
294 | friend.ParseFromString(res.tag7[i].data.data)
295 | # 更新好友信息到数据库
296 | update_contact_info(friend)
297 | return (res.continue_flag, res.sync_key_cur, res.sync_key_max)
298 |
299 | # 同步消息组包函数
300 | def new_sync_req2buf():
301 | # protobuf组包
302 | req = mm_pb2.new_sync_req(
303 | flag=mm_pb2.new_sync_req.continue_flag(flag=0),
304 | selector=7,
305 | sync_Key=Util.get_sync_key(),
306 | scene=3,
307 | device=define.__ANDROID_VER__,
308 | sync_msg_digest=1,
309 | )
310 |
311 | # 组包
312 | return pack(req.SerializeToString(), 138)
313 |
314 | # 同步消息解包函数
315 | def new_sync_buf2resp(buf):
316 | # 解包
317 | res = mm_pb2.new_sync_resp()
318 | res.ParseFromString(UnPack(buf))
319 |
320 | # 刷新sync key
321 | Util.set_sync_key(res.sync_key)
322 | logger.debug('newsync sync_key len:{}\ndata:{}'.format(
323 | len(res.sync_key), Util.b2hex(res.sync_key)))
324 |
325 | # 解析
326 | for i in range(res.msg.cntList):
327 | if 5 == res.msg.tag2[i].type: # 未读消息
328 | msg = mm_pb2.Msg()
329 | msg.ParseFromString(res.msg.tag2[i].data.data)
330 | if 9999 == msg.type: # 过滤系统垃圾消息
331 | continue
332 | if 10002 == msg.type and 'weixin' == msg.from_id.id: # 过滤系统垃圾消息
333 | continue
334 | else:
335 | # 将消息存入数据库
336 | Util.insert_msg_to_db(msg.serverid, msg.createTime, msg.from_id.id, msg.to_id.id, msg.type, msg.raw.content)
337 | logger.info('收到新消息:\ncreate utc time:{}\ntype:{}\nfrom:{}\nto:{}\nraw data:{}\nxml data:{}'.format(Util.utc_to_local_time(msg.createTime), msg.type, msg.from_id.id, msg.to_id.id, msg.raw.content, msg.xmlContent))
338 | # 接入插件
339 | plugin.dispatch(msg)
340 | elif 2 == res.msg.tag2[i].type: # 更新好友消息
341 | friend = mm_pb2.contact_info()
342 | friend.ParseFromString(res.msg.tag2[i].data.data)
343 | # 更新好友信息到数据库
344 | update_contact_info(friend)
345 | return
346 |
347 | # 通知服务器消息已接收(无返回数据)(仅用于长链接)
348 | def sync_done_req2buf():
349 | # 取sync key
350 | sync_key = mm_pb2.SyncKey()
351 | sync_key.ParseFromString(Util.get_sync_key())
352 | # 包体
353 | body = sync_key.msgkey.SerializeToString()
354 | # 包头:固定8字节,网络字节序;4字节本地时间与上次newsync时间差(us);4字节protobuf长度
355 | header = b'\x00\x00\x00\xff' + struct.pack(">I", len(body))
356 | # 组包
357 | send_data = header + body
358 | logger.debug('report kv数据:{}'.format(Util.b2hex(send_data)))
359 | return send_data
360 |
361 | # 发送文字消息请求(名片和小表情[微笑])
362 | def new_send_msg_req2buf(to_wxid, msg_content, at_user_list = [], msg_type=1):
363 | # protobuf组包
364 | req = mm_pb2.new_send_msg_req(
365 | cnt=1, # 本次发送消息数量(默认1条)
366 | msg=mm_pb2.new_send_msg_req.msg_info(
367 | to=mm_pb2.Wxid(id=to_wxid),
368 | content=msg_content, # 消息内容
369 | type=msg_type, # 默认发送文字消息,type=1
370 | utc=Util.get_utc(),
371 | client_id=Util.get_utc() + random.randint(0, 0xFFFF) # 确保不重复
372 | )
373 | )
374 | # 群聊at功能
375 | if len(at_user_list):
376 | user_list = ''.join(["{},".format(x) for x in at_user_list]).strip(',')
377 | req.msg.at_list = ''.format(user_list)
378 | # 组包
379 | return pack(req.SerializeToString(), 522)
380 |
381 | # 发送文字消息解包函数
382 | def new_send_msg_buf2resp(buf):
383 | # 解包
384 | res = mm_pb2.new_send_msg_resp()
385 | res.ParseFromString(UnPack(buf))
386 | # 消息发送结果
387 | if res.res.code: # -44被删好友,-22被拉黑;具体提示系统会发type=10000的通知
388 | logger.info('消息发送失败,错误码:{}'.format(res.res.code))
389 | else:
390 | logger.debug('消息发送成功,svrid:{}'.format(res.res.svrid))
391 |
392 | return (res.res.code, res.res.svrid)
393 |
394 | # 分享链接组包函数
395 | def send_app_msg_req2buf(wxid, title, des, link_url, thumb_url):
396 | # protobuf组包
397 | req = mm_pb2.new_send_app_msg_req(
398 | login=mm_pb2.LoginInfo(
399 | aesKey=Util.sessionKey,
400 | uin=Util.uin,
401 | guid=define.__GUID__ + '\0', # guid以\0结尾
402 | clientVer=define.__CLIENT_VERSION__,
403 | androidVer=define.__ANDROID_VER__,
404 | unknown=0,
405 | ),
406 | info=mm_pb2.new_send_app_msg_req.appmsg_info(
407 | from_wxid=Util.wxid,
408 | app_wxid='',
409 | tag3=0,
410 | to_wxid=wxid,
411 | type=5,
412 | content=define.SHARE_LINK.format(title, des, link_url, thumb_url),
413 | utc=Util.get_utc(),
414 | client_id='{}{}{}{}'.format(wxid, random.randint(
415 | 1, 99), 'T', Util.get_utc()*1000 + random.randint(1, 999)),
416 | tag10=3,
417 | tag11=0,
418 | ),
419 | tag4=0,
420 | tag6=0,
421 | tag7='',
422 | fromScene='',
423 | tag9=0,
424 | tag10=0,
425 | )
426 | # 组包
427 | return pack(req.SerializeToString(), 222), req.info.content, req.info.client_id
428 |
429 | # 分享链接解包函数
430 | def send_app_msg_buf2resp(buf):
431 | # 解包
432 | res = mm_pb2.new_send_app_msg_resp()
433 | res.ParseFromString(UnPack(buf))
434 | logger.debug('分享链接发送结果:{},svrid:{}'.format(res.tag1.len, res.svrid))
435 | return (res.tag1.len, res.svrid)
436 |
437 | #好友操作请求
438 | def verify_user_req2buf(opcode,user_wxid,user_v1_name,user_ticket,user_anti_ticket,send_content):
439 | #protobuf组包
440 | req = mm_pb2.verify_user_req(
441 | login = mm_pb2.LoginInfo(
442 | aesKey = Util.sessionKey,
443 | uin = Util.uin,
444 | guid = define.__GUID__ + '\0', #guid以\0结尾
445 | clientVer = define.__CLIENT_VERSION__,
446 | androidVer = define.__ANDROID_VER__,
447 | unknown = 0,
448 | ),
449 | op_code = opcode,
450 | tag3 = 1,
451 | user = mm_pb2.verify_user_req.user_info(
452 | wxid = user_v1_name,
453 | ticket = user_ticket,
454 | anti_ticket = user_anti_ticket,
455 | tag4 = 0,
456 | tag8 = 0,
457 | ),
458 | content = send_content,
459 | tag6 = 1,
460 | scene = b'\x06',
461 | )
462 | #组包
463 | return pack(req.SerializeToString(),30)
464 |
465 | #好友操作结果
466 | def verify_user_msg_buf2resp(buf):
467 | #解包
468 | res = mm_pb2.verify_user_resp()
469 | res.ParseFromString(UnPack(buf))
470 | logger.info('好友操作返回结果:{},wxid:{}'.format(res.res.code,res.wxid))
471 | #返回对方wxid
472 | return res.wxid
473 |
474 | #收红包请求1(获取timingIdentifier)
475 | def recieve_wxhb_req2buf(channelId,msgType,nativeUrl,sendId,inWay = 1,ver='v1.0'):
476 | #protobuf组包
477 | wxhb_info = 'channelId={}&inWay={}&msgType={}&nativeUrl={}&sendId={}&ver={}'.format(channelId,inWay,msgType,urllib.parse.quote(nativeUrl),sendId,ver)
478 | req = mm_pb2.receive_wxhb_req(
479 | login = mm_pb2.LoginInfo(
480 | aesKey = Util.sessionKey,
481 | uin = Util.uin,
482 | guid = define.__GUID__ + '\0', #guid以\0结尾
483 | clientVer = define.__CLIENT_VERSION__,
484 | androidVer = define.__ANDROID_VER__,
485 | unknown = 0,
486 | ),
487 | cmd = -1,
488 | tag3 = 1,
489 | info = mm_pb2.mmStr(
490 | len = len(wxhb_info),
491 | str = wxhb_info,
492 | )
493 | )
494 | #组包
495 | return pack(req.SerializeToString(),1581)
496 |
497 | #收红包请求1响应[返回:(timingIdentifier,sendUser_wxid)]
498 | def recieve_wxhb_buf2resp(buf):
499 | res = mm_pb2.receive_wxhb_resp()
500 | res.ParseFromString(UnPack(buf))
501 | logger.debug('红包详细信息:' + res.hb_info.str)
502 | try:
503 | if not res.ret_code and 'ok' == res.ret_msg:
504 | #解析timingIdentifier
505 | info = eval(res.hb_info.str)
506 | logger.info('[recieve_wxhb_buf2resp]timingIdentifier={},sendUserName={}'.format(info['timingIdentifier'],info['sendUserName']))
507 | return (info['timingIdentifier'],info['sendUserName'])
508 | except:
509 | pass
510 | logger.info('[recieve_wxhb_buf2resp]请求timingIdentifier失败,err_code={},err_msg:{}'.format(res.ret_code,res.ret_msg))
511 | return ('','')
512 |
513 | # 收红包请求2(拆红包)
514 | def open_wxhb_req2buf(channelId,msgType,nativeUrl,sendId,sessionUserName,timingIdentifier,ver='v1.0'):
515 | #protobuf组包
516 | wxhb_info = 'channelId={}&msgType={}&nativeUrl={}&sendId={}&sessionUserName={}&timingIdentifier={}&ver={}'.format(channelId,msgType,urllib.parse.quote(nativeUrl),sendId,sessionUserName,timingIdentifier,ver)
517 | req = mm_pb2.open_wxhb_req(
518 | login = mm_pb2.LoginInfo(
519 | aesKey = Util.sessionKey,
520 | uin = Util.uin,
521 | guid = define.__GUID__ + '\0', #guid以\0结尾
522 | clientVer = define.__CLIENT_VERSION__,
523 | androidVer = define.__ANDROID_VER__,
524 | unknown = 0,
525 | ),
526 | cmd = -1,
527 | tag3 = 1,
528 | info = mm_pb2.mmStr(
529 | len = len(wxhb_info),
530 | str = wxhb_info,
531 | )
532 | )
533 | #组包
534 | return pack(req.SerializeToString(),1685)
535 |
536 | # 收红包请求2响应
537 | def open_wxhb_buf2resp(buf):
538 | res = mm_pb2.open_wxhb_resp()
539 | res.ParseFromString(UnPack(buf))
540 | logger.debug('[红包领取结果]错误码={},详细信息:{}'.format(res.ret_code,res.res.str))
541 | return (res.ret_code,res.res.str)
542 |
543 | # 查看红包信息请求
544 | def qry_detail_wxhb_req2buf(nativeUrl, sendId, limit = 11, offset = 0, ver='v1.0'):
545 | #protobuf组包
546 | wxhb_info = 'limit={}&nativeUrl={}&offset={}&sendId={}&ver={}'.format(limit, urllib.parse.quote(nativeUrl), offset, sendId, ver)
547 | req = mm_pb2.qry_detail_wxhb_req(
548 | login = mm_pb2.LoginInfo(
549 | aesKey = Util.sessionKey,
550 | uin = Util.uin,
551 | guid = define.__GUID__ + '\0', #guid以\0结尾
552 | clientVer = define.__CLIENT_VERSION__,
553 | androidVer = define.__ANDROID_VER__,
554 | unknown = 0,
555 | ),
556 | cmd = -1,
557 | tag3 = 1,
558 | info = mm_pb2.mmStr(
559 | len = len(wxhb_info),
560 | str = wxhb_info,
561 | )
562 | )
563 | return pack(req.SerializeToString(),1585)
564 |
565 | # 查看红包信息响应
566 | def qry_detail_wxhb_buf2resp(buf):
567 | res = mm_pb2.qry_detail_wxhb_resp()
568 | res.ParseFromString(UnPack(buf))
569 | logger.debug('[红包领取信息]错误码={},详细信息:{}'.format(res.ret_code,res.res.str))
570 | return (res.ret_code,res.res.str)
571 |
572 | # 发送emoji表情
573 | def send_emoji_req2buf(wxid, file_name, game_type, content):
574 | #protobuf组包
575 | req = mm_pb2.send_emoji_req(
576 | login = mm_pb2.LoginInfo(
577 | aesKey = Util.sessionKey,
578 | uin = Util.uin,
579 | guid = define.__GUID__ + '\0', #guid以\0结尾
580 | clientVer = define.__CLIENT_VERSION__,
581 | androidVer = define.__ANDROID_VER__,
582 | unknown = 0,
583 | ),
584 | tag2 = 1,
585 | emoji = mm_pb2.send_emoji_req.emoji_info(
586 | animation_id = file_name, # emoji 加密文件名
587 | tag2 = 0,
588 | tag3 = random.randint(1, 9999),
589 | tag4 = mm_pb2.send_emoji_req.emoji_info.TAG4(tag1 = 0),
590 | tag5 = 1,
591 | to_wxid = wxid,
592 | game_ext = ''.format(game_type, content),
593 | tag8 = '',
594 | utc = '{}'.format(Util.get_utc() * 1000 + random.randint(1, 999)),
595 | tag11 = 0,
596 | ),
597 | tag4 = 0,
598 | )
599 | #组包
600 | return pack(req.SerializeToString(), 175), req.emoji.utc
601 |
602 | # 发送emoji表情响应
603 | def send_emoji_buf2resp(buf):
604 | res = mm_pb2.send_emoji_resp()
605 | res.ParseFromString(UnPack(buf))
606 | if res.res.code: # emoji发送失败
607 | logger.info('emoji发送失败, 错误码={}'.format(res.res.code), 11)
608 | return res.res.code, res.res.svrid
609 |
610 | # 收款请求
611 | def transfer_operation_req2buf(invalid_time, trans_id, transaction_id, user_name):
612 | #protobuf组包
613 | transfer_operation = 'invalid_time={}&op=confirm&total_fee=0&trans_id={}&transaction_id={}&username={}'.format(invalid_time, trans_id, transaction_id, user_name)
614 | transfer_operation_with_sign = 'invalid_time={}&op=confirm&total_fee=0&trans_id={}&transaction_id={}&username={}&WCPaySign={}'.format(invalid_time, trans_id, transaction_id, user_name, Util.SignWith3Des(transfer_operation))
615 | req = mm_pb2.transfer_operation_req(
616 | login = mm_pb2.LoginInfo(
617 | aesKey = Util.sessionKey,
618 | uin = Util.uin,
619 | guid = define.__GUID__ + '\0', #guid以\0结尾
620 | clientVer = define.__CLIENT_VERSION__,
621 | androidVer = define.__ANDROID_VER__,
622 | unknown = 0,
623 | ),
624 | tag2 = 0,
625 | tag3 = 1,
626 | info = mm_pb2.mmStr(
627 | len = len(transfer_operation_with_sign),
628 | str = transfer_operation_with_sign,
629 | )
630 | )
631 | #组包
632 | return pack(req.SerializeToString(), 385)
633 |
634 | # 收款结果
635 | def transfer_operation_buf2resp(buf):
636 | res = mm_pb2.transfer_operation_resp()
637 | res.ParseFromString(UnPack(buf))
638 | logger.debug('[收款结果]错误码={},详细信息:{}'.format(res.ret_code,res.res.str))
639 | return (res.ret_code,res.res.str)
640 |
641 | # 查询转账记录请求
642 | def transfer_query_req2buf(invalid_time, trans_id, transfer_id):
643 | #protobuf组包
644 | transfer_info = 'invalid_time={}&trans_id={}&transfer_id={}'.format(invalid_time, trans_id, transfer_id)
645 | transfer_info_with_sign = 'invalid_time={}&trans_id={}&transfer_id={}&WCPaySign={}'.format(invalid_time, trans_id, transfer_id, Util.SignWith3Des(transfer_info))
646 | req = mm_pb2.transfer_query_req(
647 | login = mm_pb2.LoginInfo(
648 | aesKey = Util.sessionKey,
649 | uin = Util.uin,
650 | guid = define.__GUID__ + '\0', #guid以\0结尾
651 | clientVer = define.__CLIENT_VERSION__,
652 | androidVer = define.__ANDROID_VER__,
653 | unknown = 0,
654 | ),
655 | tag2 = 0,
656 | tag3 = 1,
657 | info = mm_pb2.mmStr(
658 | len = len(transfer_info_with_sign),
659 | str = transfer_info_with_sign,
660 | )
661 | )
662 | #组包
663 | return pack(req.SerializeToString(), 385)
664 |
665 | # 查询转账记录结果
666 | def transfer_query_buf2resp(buf):
667 | res = mm_pb2.transfer_query_resp()
668 | res.ParseFromString(UnPack(buf))
669 | logger.debug('[查询转账记录]错误码={},详细信息:{}'.format(res.ret_code,res.res.str))
670 | return (res.ret_code,res.res.str)
671 |
672 | # 获取好友信息请求
673 | def get_contact_req2buf(wxid):
674 | #protobuf组包
675 | req = mm_pb2.get_contact_req(
676 | login = mm_pb2.LoginInfo(
677 | aesKey = Util.sessionKey,
678 | uin = Util.uin,
679 | guid = define.__GUID__ + '\0', #guid以\0结尾
680 | clientVer = define.__CLIENT_VERSION__,
681 | androidVer = define.__ANDROID_VER__,
682 | unknown = 0,
683 | ),
684 | tag2 = 1,
685 | wxid = mm_pb2.Wxid(id = wxid),
686 | tag4 = 0,
687 | tag6 = 1,
688 | tag7 = mm_pb2.get_contact_req.TAG7(),
689 | tag8 = 0,
690 | )
691 | # 组包
692 | return pack(req.SerializeToString(), 182)
693 |
694 | # 获取好友信息响应
695 | def get_contact_buf2resp(buf):
696 | res = mm_pb2.get_contact_resp()
697 | res.ParseFromString(UnPack(buf))
698 | # 显示好友信息
699 | update_contact_info(res.info, False)
700 | return res.info, res.ticket
701 |
702 | # 建群聊请求
703 | def create_chatroom_req2buf(group_member_list):
704 | #protobuf组包
705 | req = mm_pb2.create_chatroom_req(
706 | login = mm_pb2.LoginInfo(
707 | aesKey = Util.sessionKey,
708 | uin = Util.uin,
709 | guid = define.__GUID__ + '\0', #guid以\0结尾
710 | clientVer = define.__CLIENT_VERSION__,
711 | androidVer = define.__ANDROID_VER__,
712 | unknown = 0,
713 | ),
714 | tag2 = mm_pb2.create_chatroom_req.TAG2(),
715 | member_cnt = len(group_member_list),
716 | tag5 = 0,
717 | )
718 | # 添加群成员
719 | for wxid in group_member_list:
720 | member = req.member.add()
721 | member.wxid.id = wxid
722 | # 组包
723 | return pack(req.SerializeToString(), 119)
724 |
725 | # 建群聊响应
726 | def create_chatroom_buf2resp(buf):
727 | res = mm_pb2.create_chatroom_resp()
728 | res.ParseFromString(UnPack(buf))
729 | if not res.res.code and res.chatroom_wxid.id:
730 | logger.info('建群成功!群聊wxid:{}'.format(res.chatroom_wxid.id), 11)
731 | else:
732 | logger.info('建群失败!\n错误码:{}\n错误信息:{}'.format(res.res.code, res.res.msg.msg), 5)
733 | return res.chatroom_wxid.id
734 |
735 | # 面对面建群请求
736 | def mm_facing_create_chatroom_req2buf(op_code, pwd = '9999', lon = 116.39, lat = 39.90):
737 | #protobuf组包
738 | req = mm_pb2.mm_facing_create_chatroom_req(
739 | login = mm_pb2.LoginInfo(
740 | aesKey = Util.sessionKey,
741 | uin = Util.uin,
742 | guid = define.__GUID__ + '\0', #guid以\0结尾
743 | clientVer = define.__CLIENT_VERSION__,
744 | androidVer = define.__ANDROID_VER__,
745 | unknown = 0,
746 | ),
747 | op_code = op_code,
748 | chatroom_pwd = pwd,
749 | lon = lon,
750 | lat = lat,
751 | tag6 = 1,
752 | tag9 = 0,
753 | )
754 | # 组包
755 | return pack(req.SerializeToString(), 653)
756 |
757 | # 面对面建群响应
758 | def mm_facing_create_chatroom_buf2resp(buf, op_code):
759 | res = mm_pb2.mm_facing_create_chatroom_resp()
760 | res.ParseFromString(UnPack(buf))
761 | if 0 == op_code:
762 | if res.res.code:
763 | logger.info('面对面建群步骤1失败!\n错误码:{}\n错误信息:{}'.format(res.res.code, res.res.msg.msg), 5)
764 | return res.res.code, ''
765 | else:
766 | if not res.res.code and res.wxid:
767 | logger.info('面对面建群成功! {} 面对面群聊wxid:{}'.format(res.res.msg.msg, res.wxid), 11)
768 | else:
769 | logger.info('面对面建群步骤2失败!\n错误码:{}\n错误信息:{}'.format(res.res.code, res.res.msg.msg), 5)
770 | return res.res.code, res.wxid
771 |
772 | # 群聊拉人请求
773 | def add_chatroom_member_req2buf(chatroom_wxid, member_list):
774 | #protobuf组包
775 | req = mm_pb2.add_chatroom_member_req(
776 | login = mm_pb2.LoginInfo(
777 | aesKey = Util.sessionKey,
778 | uin = Util.uin,
779 | guid = define.__GUID__ + '\0', #guid以\0结尾
780 | clientVer = define.__CLIENT_VERSION__,
781 | androidVer = define.__ANDROID_VER__,
782 | unknown = 0,
783 | ),
784 | member_cnt = len(member_list),
785 | chatroom_wxid = mm_pb2.add_chatroom_member_req.chatroom_info(wxid = chatroom_wxid),
786 | tag5 = 0,
787 | )
788 | # 添加群成员
789 | for wxid in member_list:
790 | member = req.member.add()
791 | member.wxid.id = wxid
792 | # 组包
793 | return pack(req.SerializeToString(), 120)
794 |
795 | # 群聊拉人响应
796 | def add_chatroom_member_buf2resp(buf):
797 | res = mm_pb2.mm_facing_create_chatroom_resp()
798 | res.ParseFromString(UnPack(buf))
799 | return (res.res.code, res.res.msg.msg)
800 |
801 | # 设置群聊昵称请求
802 | def set_group_nick_name_req2buf(chatroom_wxid, nick_name):
803 | # protobuf组包
804 | req = mm_pb2.oplog_req(
805 | tag1 = mm_pb2.oplog_req.TAG1(
806 | tag1 = 1,
807 | cmd = mm_pb2.oplog_req.TAG1.CMD(
808 | cmd_id = 48,
809 | option = mm_pb2.oplog_req.TAG1.CMD.OPTION(
810 | data = mm_pb2.op_set_group_nick_name(
811 | tag1 = chatroom_wxid,
812 | tag2 = Util.wxid,
813 | tag3 = nick_name,
814 | ).SerializeToString(),
815 | ),
816 | ),
817 | ),
818 | )
819 | req.tag1.cmd.option.len = len(req.tag1.cmd.option.data)
820 | # 组包
821 | return pack(req.SerializeToString(), 681)
822 |
823 | # 设置群聊昵称响应
824 | def set_group_nick_name_buf2resp(buf):
825 | res = mm_pb2.oplog_resp()
826 | res.ParseFromString(UnPack(buf))
827 | try:
828 | code,__ = decoder._DecodeVarint(res.res.code, 0)
829 | if code:
830 | logger.info('设置群昵称失败,错误码:0x{:x},错误信息:{}'.format(code, res.res.msg), 11)
831 | except:
832 | code = -1
833 | logger.info('设置群昵称失败!', 11)
834 | return code
835 |
836 | # 消息撤回请求
837 | def revoke_msg_req2buf(wxid, svrid):
838 | # 在数据库中查询svrid对应的client_msg_id
839 | client_msg_id = Util.get_client_msg_id(svrid)
840 | #protobuf组包
841 | req = mm_pb2.revoke_msg_req(
842 | login = mm_pb2.LoginInfo(
843 | aesKey = Util.sessionKey,
844 | uin = Util.uin,
845 | guid = define.__GUID__ + '\0', #guid以\0结尾
846 | clientVer = define.__CLIENT_VERSION__,
847 | androidVer = define.__ANDROID_VER__,
848 | unknown = 0,
849 | ),
850 | client_id = client_msg_id,
851 | new_client_id = 0,
852 | utc = Util.get_utc(),
853 | tag5 = 0,
854 | from_wxid = Util.wxid,
855 | to_wxid = wxid,
856 | index_of_request = 0, # 自增1
857 | svrid = svrid,
858 | )
859 | # 组包
860 | return pack(req.SerializeToString(), 594)
861 |
862 | # 消息撤回响应
863 | def revoke_msg_buf2resp(buf):
864 | res = mm_pb2.revoke_msg_resp()
865 | res.ParseFromString(UnPack(buf))
866 | logger.info('[消息撤回]错误码:{},提示信息:{}'.format(res.res.code, res.response_sys_wording), 11)
867 | return res.res.code
868 |
869 | # 好友(群聊)操作请求:删除/保存/拉黑/恢复/设置群昵称/设置好友备注名
870 | def op_friend_req2buf(friend_info, cmd = 2):
871 | #protobuf组包
872 | req = mm_pb2.oplog_req(
873 | tag1 = mm_pb2.oplog_req.TAG1(
874 | tag1 = 1,
875 | cmd = mm_pb2.oplog_req.TAG1.CMD(
876 | cmd_id = cmd,
877 | option = mm_pb2.oplog_req.TAG1.CMD.OPTION(
878 | data = friend_info,
879 | ),
880 | ),
881 | ),
882 | )
883 | req.tag1.cmd.option.len = len(req.tag1.cmd.option.data)
884 | # 组包
885 | return pack(req.SerializeToString(), 681)
886 |
887 | # 好友(群聊)操作结果
888 | def op_friend_buf2resp(buf):
889 | res = mm_pb2.oplog_resp()
890 | res.ParseFromString(UnPack(buf))
891 | try:
892 | code,__ = decoder._DecodeVarint(res.res.code, 0)
893 | if code:
894 | logger.info('好友操作失败,错误码:0x{:x},错误信息:{}'.format(code, res.res.msg), 11)
895 | except:
896 | code = -1
897 | logger.info('好友操作失败!', 11)
898 | return code
899 |
900 | # 发布群公告请求(仅限群主;自动@所有人)
901 | def set_chatroom_announcement_req2buf(wxid, text):
902 | #protobuf组包
903 | req = mm_pb2.set_chatroom_announcement_req(
904 | login = mm_pb2.LoginInfo(
905 | aesKey = Util.sessionKey,
906 | uin = Util.uin,
907 | guid = define.__GUID__ + '\0', #guid以\0结尾
908 | clientVer = define.__CLIENT_VERSION__,
909 | androidVer = define.__ANDROID_VER__,
910 | unknown = 0,
911 | ),
912 | chatroom_wxid = wxid,
913 | content = text,
914 | )
915 | # 组包
916 | return pack(req.SerializeToString(), 993)
917 |
918 | # 发布群公告响应
919 | def set_chatroom_announcement_buf2resp(buf):
920 | res = mm_pb2.set_chatroom_announcement_resp()
921 | res.ParseFromString(UnPack(buf))
922 | if res.res.code:
923 | logger.info('发布群公告失败!错误码:{},提示信息:{}'.format(res.res.code, res.res.message), 11)
924 | return res.res.code
--------------------------------------------------------------------------------
/microchat/client_tornado.py:
--------------------------------------------------------------------------------
1 | from tornado import gen, iostream
2 | from tornado.tcpclient import TCPClient
3 | from tornado.ioloop import IOLoop
4 | from tornado.ioloop import PeriodicCallback
5 | import struct
6 | from . import dns_ip
7 | from . import interface
8 | from . import Util
9 | from . import business
10 | from .plugin.logger_wrapper import logger
11 |
12 | #recv缓冲区大小
13 | BUFFSIZE = 4096
14 |
15 | #心跳包seq id
16 | HEARTBEAT_SEQ = 0xFFFFFFFF
17 |
18 | #长链接确认包seq id
19 | IDENTIFY_SEQ = 0xFFFFFFFE
20 |
21 | #推送消息seq id
22 | PUSH_SEQ = 0
23 |
24 | #cmd id
25 | CMDID_NOOP_REQ = 6 #心跳
26 | CMDID_IDENTIFY_REQ = 205 #长链接确认
27 | CMDID_MANUALAUTH_REQ = 253 #登录
28 | CMDID_PUSH_ACK = 24 #推送通知
29 | CMDID_REPORT_KV_REQ = 1000000190 #通知服务器消息已接收
30 |
31 | #解包结果
32 | UNPACK_NEED_RESTART = -2 # 需要切换DNS重新登陆
33 | UNPACK_FAIL = -1 #解包失败
34 | UNPACK_CONTINUE = 0 #封包不完整,继续接收数据
35 | UNPACK_OK = 1 #解包成功
36 |
37 |
38 | def recv_data_handler(recv_data):
39 | # logger.debug("tornado recv: ", recv_data)
40 | pass
41 |
42 |
43 | #心跳时间间隔(秒)
44 | HEARTBEAT_TIMEOUT = 60
45 |
46 |
47 | class ChatClient(object):
48 | """docstring for ChatClient"""
49 |
50 | def __init__(self, ioloop, recv_cb, host, port, usr_name, passwd):
51 | self.ioloop = ioloop
52 | self.recv_cb = recv_cb
53 | self.host = host
54 | self.port = port
55 | self.usr_name = usr_name
56 | self.passwd = passwd
57 | self.last_heartbeat_time = 0
58 | self.cnt = 0
59 | #封包编号(递增1)
60 | self.seq = 1
61 | self.login_aes_key = b''
62 | self.recv_data = b''
63 | self.heartbeat_callback = None
64 |
65 | @gen.coroutine
66 | def start(self):
67 | wait_sec = 10
68 | while True:
69 | try:
70 | self.stream = yield TCPClient().connect(self.host, self.port)
71 | break
72 | except iostream.StreamClosedError:
73 | logger.error("connect error and again")
74 | yield gen.sleep(wait_sec)
75 | wait_sec = (wait_sec if (wait_sec >= 60) else (wait_sec * 2))
76 |
77 | self.send_heart_beat()
78 | self.heartbeat_callback = PeriodicCallback(self.send_heart_beat, 1000 * HEARTBEAT_TIMEOUT)
79 | self.heartbeat_callback.start() # start scheduler
80 | self.login()
81 | self.stream.read_bytes(16, self.__recv_header)
82 |
83 | @gen.coroutine
84 | def restart(self, host, port):
85 | if self.heartbeat_callback:
86 | # 停止心跳
87 | self.heartbeat_callback.stop()
88 | self.host = host
89 | self.port = port
90 | self.stream.set_close_callback(self.__closed)
91 | yield self.stream.close()
92 |
93 | def __closed(self):
94 | self.start()
95 |
96 | def send_heart_beat(self):
97 | logger.debug('last_heartbeat_time = {}, elapsed_time = {}'.format(self.last_heartbeat_time, Util.get_utc() - self.last_heartbeat_time))
98 | #判断是否需要发送心跳包
99 | if (Util.get_utc() - self.last_heartbeat_time) >= HEARTBEAT_TIMEOUT:
100 | #长链接发包
101 | send_data = self.pack(CMDID_NOOP_REQ)
102 | self.send(send_data)
103 | #记录本次发送心跳时间
104 | self.last_heartbeat_time = Util.get_utc()
105 | return True
106 | else:
107 | return False
108 |
109 | def login(self):
110 | (login_buf, self.login_aes_key) = business.login_req2buf(
111 | self.usr_name, self.passwd)
112 | send_data = self.pack(CMDID_MANUALAUTH_REQ, login_buf)
113 | self.send(send_data)
114 |
115 | @gen.coroutine
116 | def __recv_header(self, data):
117 | self.cnt += 1
118 | self.recv_data = data
119 | logger.debug('recive from the server', data)
120 | (len_ack, _, _) = struct.unpack('>I4xII', data)
121 | if self.recv_cb:
122 | self.recv_cb(data)
123 | try:
124 | yield self.stream.read_bytes(len_ack - 16, self.__recv_payload)
125 | except iostream.StreamClosedError:
126 | logger.error("stream read error, TCP disconnect and restart")
127 | self.restart(dns_ip.fetch_longlink_ip(), 443)
128 |
129 | @gen.coroutine
130 | def __recv_payload(self, data):
131 | logger.debug('recive from the server', data)
132 | if self.recv_cb:
133 | self.recv_cb(data)
134 | self.recv_data += data
135 | if data != b'':
136 | (ret, buf) = self.unpack(self.recv_data)
137 | if UNPACK_OK == ret:
138 | while UNPACK_OK == ret:
139 | (ret, buf) = self.unpack(buf)
140 | #刷新心跳
141 | self.send_heart_beat()
142 | if UNPACK_NEED_RESTART == ret: # 需要切换DNS重新登陆
143 | if dns_ip.dns_retry_times > 0:
144 | self.restart(dns_ip.fetch_longlink_ip(),443)
145 | return
146 | else:
147 | logger.error('切换DNS尝试次数已用尽,程序即将退出............')
148 | self.stop()
149 | try:
150 | yield self.stream.read_bytes(16, self.__recv_header)
151 | except iostream.StreamClosedError:
152 | logger.error("stream read error, TCP disconnect and restart")
153 | self.restart(dns_ip.fetch_longlink_ip(), 443)
154 |
155 | def send(self, data):
156 | try:
157 | self.stream.write(data)
158 | except iostream.StreamClosedError:
159 | logger.error("stream write error, TCP disconnect and restart")
160 | self.restart(dns_ip.fetch_longlink_ip(), 443)
161 |
162 | def stop(self):
163 | self.ioloop.stop()
164 |
165 | #长链接组包
166 | def pack(self, cmd_id, buf=b''):
167 | header = bytearray(0)
168 | header += struct.pack(">I", len(buf) + 16)
169 | header += b'\x00\x10'
170 | header += b'\x00\x01'
171 | header += struct.pack(">I", cmd_id)
172 | if CMDID_NOOP_REQ == cmd_id: #心跳包
173 | header += struct.pack(">I", HEARTBEAT_SEQ)
174 | elif CMDID_IDENTIFY_REQ == cmd_id:
175 | header += struct.pack(
176 | ">I", IDENTIFY_SEQ
177 | ) #登录确认包(暂未实现;该请求确认成功后服务器会直接推送消息内容,否则服务器只下发推送通知,需要主动同步消息)
178 | else:
179 | header += struct.pack(">I", self.seq)
180 | #封包编号自增
181 | self.seq += 1
182 |
183 | return header + buf
184 |
185 | #长链接解包
186 | def unpack(self, buf):
187 | if len(buf) < 16: #包头不完整
188 | return (UNPACK_CONTINUE,b'')
189 | else:
190 | #解析包头
191 | header = buf[:16]
192 | (len_ack, cmd_id_ack, seq_id_ack) = struct.unpack('>I4xII', header)
193 | logger.debug('封包长度:{},cmd_id:{},seq_id:0x{:x}'.format(len_ack, cmd_id_ack,
194 | seq_id_ack))
195 | #包长合法性验证
196 | if len(buf) < len_ack: #包体不完整
197 | return (UNPACK_CONTINUE, b'')
198 | else:
199 | if CMDID_PUSH_ACK == cmd_id_ack and PUSH_SEQ == seq_id_ack: #推送通知:服务器有新消息
200 | #尝试获取sync key
201 | sync_key = Util.get_sync_key()
202 | if sync_key: #sync key存在
203 | try:
204 | interface.new_sync() #使用newsync同步消息
205 | except RuntimeError as e:
206 | logger.error(e)
207 | self.ioloop.stop()
208 | return (UNPACK_FAIL, b'')
209 |
210 | self.send(
211 | self.pack(CMDID_REPORT_KV_REQ,
212 | business.sync_done_req2buf())) #通知服务器消息已接收
213 | else: #sync key不存在
214 | interface.new_init() #使用短链接newinit初始化sync key
215 | else:
216 | cmd_id = cmd_id_ack - 1000000000
217 | if CMDID_NOOP_REQ == cmd_id: #心跳响应
218 | pass
219 | elif CMDID_MANUALAUTH_REQ == cmd_id: #登录响应
220 | code = business.login_buf2Resp(buf[16:len_ack],self.login_aes_key)
221 | if -106 == code:
222 | # 授权后,尝试自动重新登陆
223 | logger.info('正在重新登陆........................', 14)
224 | self.login()
225 | elif -301 == code:
226 | logger.info('正在重新登陆........................', 14)
227 | return (UNPACK_NEED_RESTART, b'')
228 | elif code:
229 | # raise RuntimeError('登录失败!') #登录失败
230 | self.ioloop.stop()
231 | return (UNPACK_FAIL, b'')
232 | return (UNPACK_OK, buf[len_ack:])
233 |
234 | return (UNPACK_OK,b'')
235 |
236 |
237 | def start(wechat_usrname, wechat_passwd):
238 | interface.init_all()
239 | ioloop = IOLoop.instance()
240 | tcp_client = ChatClient(ioloop=ioloop, usr_name=wechat_usrname, passwd=wechat_passwd, recv_cb=recv_data_handler,
241 | host=dns_ip.fetch_longlink_ip(), port=443)
242 | tcp_client.start()
243 | ioloop.start()
244 |
--------------------------------------------------------------------------------
/microchat/define.py:
--------------------------------------------------------------------------------
1 | __CLIENT_VERSION__ = 637927472
2 | GUID = "Aff0aef642a31fc2"
3 | __GUID__ = GUID[:15]
4 | __CLIENT_SEQID__ = GUID + "_1522827110765"
5 | __CLIENT_SEQID_SIGN__ = "e89b158e4bcf988ebd09eb83f5378e87"
6 | __IMEI__ = "865166024671219"
7 | __ANDROID_ID__ = "d3151233cfbb4fd4"
8 | __ANDROID_VER__ = "android-22"
9 | __MANUFACTURER__ = "iPhone"
10 | __MODELNAME__ = "X"
11 | __MOBILE_WIFI_MAC_ADDRESS__ = "01:61:19:58:78:d3"
12 | __AP_BSSID__ = "41:27:91:12:3e:14"
13 | __LANGUAGE__ = "zh_CN"
14 | __SOFTINFO__ = "01ARMv7 processor rev 1 (v7l) 5.1.1{}46000733776654189860012221746527381{}unknown{}2placeholder00010000000000000001{}neon vfp swp half thumb fastmult edsp vfpv3 idiva idivt{}\"wireless\"{}0\"wireless\"com.tencent.mmAndroid-x86/android_x86/x86:5.1.1/LMY48Z/denglibo08021647:userdebug/test-keysvivo v3unknown{}x86android_x86{}1{}null0wifi{}/data/data/com.tencent.mm/0010800"
15 | __DEVICEINFO__ = ""
16 |
17 | __LOGIN_RSA_VER__ = 158
18 | __LOGIN_RSA_VER158_KEY_E__ = 65537
19 | __LOGIN_RSA_VER158_KEY_N__ = "E161DA03D0B6AAD21F9A4FB27C32A3208AF25A707BB0E8ECE79506FBBAF97519D9794B7E1B44D2C6F2588495C4E040303B4C915F172DD558A49552762CB28AB309C08152A8C55A4DFC6EA80D1F4D860190A8EE251DF8DECB9B083674D56CD956FF652C3C724B9F02BE5C7CBC63FC0124AA260D889A73E91292B6A02121D25AAA7C1A87752575C181FFB25A6282725B0C38A2AD57676E0884FE20CF56256E14529BC7E82CD1F4A1155984512BD273D68F769AF46E1B0E3053816D39EB1F0588384F2F4B286E5CFAFB4D0435BDF7D3AA8D3E0C45716EAD190FDC66884B275BA08D8ED94B1F84E7729C25BD014E7FA3A23123E10D3A93B4154452DDB9EE5F8DAB67"
20 |
21 | #发送名片时,username必须为有效wxid,否则发送失败
22 | CONTACT_CARD = ''
23 |
24 | #分享链接
25 | SHARE_LINK = '{}{}view50{}00003{}000nullnull0nullnullnull00'
26 |
27 | #好友添加来源
28 | WAY = {0:'未知来源',1:'QQ',3:'微信号',10:'手机通讯录',13:'手机通讯录',14:'群聊',15:'手机号',17:'名片分享',18:'附近的人',28:'摇一摇',30:'扫一扫'}
29 |
30 | #系统服务(获取好友列表时过滤掉这些wxid)
31 | MM_DEFAULT_WXID = ("weixin","qqmail", "fmessage", "tmessage", "qmessage", "qqsync", "floatbottle", "lbsapp", "shakeapp", "medianote", "qqfriend", "newsapp", "blogapp", "facebookapp", "masssendapp", "feedsapp", "voipapp", "cardpackage", "voicevoipapp", "voiceinputapp", "officialaccounts", "linkedinplugin", "notifymessage", "appbrandcustomerservicemsg","pc_share","notification_messages","helper_entry","filehelper")
--------------------------------------------------------------------------------
/microchat/dll/ecdh_x32.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LSilent/PyMicroChat/e2862b3766f52b924d13f7a75000271dfe1e3d76/microchat/dll/ecdh_x32.dll
--------------------------------------------------------------------------------
/microchat/dll/ecdh_x64.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LSilent/PyMicroChat/e2862b3766f52b924d13f7a75000271dfe1e3d76/microchat/dll/ecdh_x64.dll
--------------------------------------------------------------------------------
/microchat/dns_ip.py:
--------------------------------------------------------------------------------
1 | '''访问'http://dns.weixin.qq.com/cgi-bin/micromsg-bin/newgetdns',
2 | 获取长ip/短ip地址.
3 | '''
4 |
5 | import requests
6 | import os
7 | import sqlite3
8 | from bs4 import BeautifulSoup
9 | from random import choice
10 |
11 | # 登录返回-301自动切换DNS
12 | dns_retry_times = 3
13 |
14 | # 短链接ip池
15 | short_ip = []
16 |
17 | # 长链接ip池
18 | long_ip = []
19 |
20 | # dns db
21 | conn_dns = None
22 |
23 | def get_ips():
24 | '''访问'http://dns.weixin.qq.com/cgi-bin/micromsg-bin/newgetdns',
25 | 返回短链接ip列表short_ip,长链接ip列表long_ip.
26 | '''
27 |
28 | ret = requests.get(
29 | 'http://dns.weixin.qq.com/cgi-bin/micromsg-bin/newgetdns')
30 | soup = BeautifulSoup(ret.text, "html.parser")
31 | short_weixin = soup.find(
32 | 'domain', attrs={'name': 'short.weixin.qq.com'})
33 | [short_ip.append(ip.get_text()) for ip in short_weixin.select('ip')]
34 | long_weixin = soup.find(
35 | 'domain', attrs={'name': 'long.weixin.qq.com'})
36 | [long_ip.append(ip.get_text()) for ip in long_weixin.select('ip')]
37 |
38 | return short_ip, long_ip
39 |
40 | # 随机取出一个长链接ip地址
41 | def fetch_longlink_ip():
42 | if not long_ip:
43 | get_ips()
44 | return choice(long_ip)
45 |
46 | # 随机取出一个短链接ip地址
47 | def fetch_shortlink_ip():
48 | if not short_ip:
49 | get_ips()
50 | return choice(short_ip)
51 |
52 | # 尝试从db加载dns
53 | def load_dns():
54 | global conn_dns,short_ip,long_ip
55 | # 建db文件夹
56 | if not os.path.exists(os.getcwd() + '/db'):
57 | os.mkdir(os.getcwd() + '/db')
58 | # 建库
59 | conn_dns = sqlite3.connect('./db/dns.db')
60 | cur = conn_dns.cursor()
61 | # 建dns表(保存上次登录时的dns)
62 | cur.execute('create table if not exists dns(host varchar(1024) unique, ip varchar(1024))')
63 | # 加载dns
64 | try:
65 | cur.execute('select ip from dns where host = "short.weixin.qq.com"')
66 | row = cur.fetchone()
67 | if row:
68 | short_ip = row[0].split(',')
69 | cur.execute('select ip from dns where host = "long.weixin.qq.com"')
70 | row = cur.fetchone()
71 | if row:
72 | long_ip = row[0].split(',')
73 | except:
74 | pass
75 | return
76 |
77 | # 保存dns到db
78 | def save_dns():
79 | try:
80 | conn_dns.commit()
81 | conn_dns.execute('delete from dns')
82 | conn_dns.execute('insert into dns(host,ip) values("{}","{}")'.format('short.weixin.qq.com', ','.join(short_ip)))
83 | conn_dns.execute('insert into dns(host,ip) values("{}","{}")'.format('long.weixin.qq.com', ','.join(long_ip)))
84 | conn_dns.commit()
85 | except:
86 | pass
87 | return
--------------------------------------------------------------------------------
/microchat/ecdh/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LSilent/PyMicroChat/e2862b3766f52b924d13f7a75000271dfe1e3d76/microchat/ecdh/__init__.py
--------------------------------------------------------------------------------
/microchat/ecdh/ecdh.py:
--------------------------------------------------------------------------------
1 | from .openssl import OpenSSL
2 | import ctypes
3 | import sys
4 | import hashlib
5 |
6 | MD5_DIGEST_LENGTH = 16
7 |
8 | def int_to_bytes(x):
9 | return x.to_bytes((x.bit_length() + 7) // 8, sys.byteorder)
10 |
11 | def int_from_bytes(xbytes):
12 | return int.from_bytes(xbytes, sys.byteorder)
13 |
14 | # 使用c接口生成ECDH本地密钥对
15 | def gen_ecdh(curve=713):
16 | # EC_KEY *ec_key = EC_KEY_new_by_curve_name(nid);
17 | ec_key = OpenSSL.EC_KEY_new_by_curve_name(curve)
18 | if ec_key == 0:
19 | raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ... " + OpenSSL.get_error())
20 |
21 | # int ret = EC_KEY_generate_key(ec_key);
22 | if (OpenSSL.EC_KEY_generate_key(ec_key)) == 0:
23 | OpenSSL.EC_KEY_free(ec_key)
24 | raise Exception("[OpenSSL] EC_KEY_generate_key FAIL ... " + OpenSSL.get_error())
25 |
26 | if (OpenSSL.EC_KEY_check_key(ec_key)) == 0:
27 | OpenSSL.EC_KEY_free(ec_key)
28 | raise Exception("[OpenSSL] EC_KEY_check_key FAIL ... " + OpenSSL.get_error())
29 |
30 | # int nLenPub = i2o_ECPublicKey(ec_key, NULL);
31 | len_pub = OpenSSL.i2o_ECPublicKey(ec_key, None)
32 | out_pub_key = ctypes.POINTER(ctypes.c_ubyte)()
33 | if (OpenSSL.i2o_ECPublicKey(ec_key, ctypes.byref(out_pub_key))) == 0:
34 | OpenSSL.EC_KEY_free(ec_key)
35 | raise Exception("[OpenSSL] i2o_ECPublicKey FAIL ... " + OpenSSL.get_error())
36 |
37 | lpub_key = [0]*len_pub
38 | for x in range(len_pub):
39 | lpub_key[x] = out_pub_key[x]
40 | pub_key = bytes(bytearray(lpub_key))
41 | OpenSSL.OPENSSL_free(out_pub_key, None, 0)
42 |
43 | # int nLenPub = i2d_ECPrivateKey(ec_key, NULL);
44 | len_pri = OpenSSL.i2d_ECPrivateKey(ec_key, None)
45 | out_pri_key = ctypes.POINTER(ctypes.c_ubyte)()
46 | if (OpenSSL.i2d_ECPrivateKey(ec_key, ctypes.byref(out_pri_key))) == 0:
47 | OpenSSL.EC_KEY_free(ec_key)
48 | raise Exception("[OpenSSL] i2d_ECPrivateKey FAIL ... " + OpenSSL.get_error())
49 |
50 | lpri_key = [0]*len_pri
51 | for y in range(len_pri):
52 | lpri_key[y] = out_pri_key[y]
53 | pri_key = bytes(bytearray(lpri_key))
54 | OpenSSL.OPENSSL_free(out_pri_key, None, 0)
55 |
56 | OpenSSL.EC_KEY_free(ec_key)
57 |
58 | return pub_key, len_pub, pri_key, len_pri
59 |
60 | # void *KDF_MD5(const void *in, size_t inlen, void *out, size_t *outlen)
61 | def kdf_md5(arr_in, in_len, arr_out, out_len):
62 | p_arr_in = ctypes.c_char_p(arr_in)
63 | p_arr_out = ctypes.c_char_p(arr_out)
64 | p_out_len = ctypes.POINTER(ctypes.c_long)(out_len)
65 | src = p_arr_in.value
66 | m = hashlib.md5()
67 | m.update(src)
68 | digest_m = m.digest()
69 | p_out_len.value = MD5_DIGEST_LENGTH
70 | p_arr_out.value = digest_m[:]
71 | return arr_out
72 |
73 | # 密钥协商
74 | def do_ecdh(curve, server_pub_key, local_pri_key):
75 |
76 | pub_ec_key = OpenSSL.EC_KEY_new_by_curve_name(curve)
77 | if pub_ec_key == 0:
78 | raise Exception("[OpenSSL] o2i_ECPublicKey FAIL ... " + OpenSSL.get_error())
79 | public_material = ctypes.c_char_p(server_pub_key)
80 | p_pub_ec_key = ctypes.c_void_p(pub_ec_key)
81 | pub_ec_key = OpenSSL.o2i_ECPublicKey(ctypes.byref(p_pub_ec_key), ctypes.byref(public_material), len(server_pub_key))
82 | if pub_ec_key == 0:
83 | OpenSSL.EC_KEY_free(pub_ec_key)
84 | raise Exception("[OpenSSL] o2i_ECPublicKey FAIL ... " + OpenSSL.get_error())
85 |
86 | pri_ec_key = OpenSSL.EC_KEY_new_by_curve_name(curve)
87 | if pri_ec_key == 0:
88 | OpenSSL.EC_KEY_free(pub_ec_key)
89 | raise Exception("[OpenSSL] d2i_ECPrivateKey FAIL ... " + OpenSSL.get_error())
90 | private_material = ctypes.c_char_p(local_pri_key)
91 | p_pri_ec_key = ctypes.c_void_p(pri_ec_key)
92 | pri_ec_key = OpenSSL.d2i_ECPrivateKey(ctypes.byref(p_pri_ec_key), ctypes.byref(private_material), len(local_pri_key))
93 | if pri_ec_key == 0:
94 | OpenSSL.EC_KEY_free(pub_ec_key)
95 | OpenSSL.EC_KEY_free(pri_ec_key)
96 | raise Exception("[OpenSSL] d2i_ECPrivateKey FAIL ... " + OpenSSL.get_error())
97 |
98 | share_key = ctypes.create_string_buffer(2048)
99 |
100 | # 回调函数
101 | # void *KDF_MD5(const void *in, size_t inlen, void *out, size_t *outlen)
102 | # KD_MD5 = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long, ctypes.c_void_p, ctypes.POINTER(ctypes.c_long))
103 | # cb = KD_MD5(kdf_md5)
104 |
105 | ret = OpenSSL.ECDH_compute_key(share_key, 28, OpenSSL.EC_KEY_get0_public_key(pub_ec_key), pri_ec_key, 0)
106 |
107 | OpenSSL.EC_KEY_free(pub_ec_key)
108 | OpenSSL.EC_KEY_free(pri_ec_key)
109 |
110 | if ret:
111 | # 对share_key取md5
112 | m = hashlib.md5()
113 | m.update(share_key.value)
114 | digest_m = m.digest()
115 |
116 | return MD5_DIGEST_LENGTH, digest_m
117 | return 0, None
118 |
--------------------------------------------------------------------------------
/microchat/ecdh/openssl.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | import ctypes
4 | import ctypes.util
5 | import platform
6 |
7 | OpenSSL = None
8 |
9 |
10 | class CipherName:
11 | def __init__(self, name, pointer, blocksize):
12 | self._name = name
13 | self._pointer = pointer
14 | self._blocksize = blocksize
15 |
16 | def __str__(self):
17 | return ("Cipher : %s | Blocksize : %s | Fonction pointer : %s" %
18 | (self._name, str(self._blocksize), str(self._pointer)))
19 |
20 | def get_pointer(self):
21 | return self._pointer()
22 |
23 | def get_name(self):
24 | return self._name
25 |
26 | def get_blocksize(self):
27 | return self._blocksize
28 |
29 |
30 | class _OpenSSL:
31 | """
32 | Wrapper for OpenSSL using ctypes
33 | """
34 | def __init__(self, library):
35 | """
36 | Build the wrapper
37 | """
38 | self._lib = ctypes.CDLL(library)
39 |
40 | self.pointer = ctypes.pointer
41 | self.c_int = ctypes.c_int
42 | self.byref = ctypes.byref
43 | self.create_string_buffer = ctypes.create_string_buffer
44 |
45 | self.ERR_error_string = self._lib.ERR_error_string
46 | self.ERR_error_string.restype = ctypes.c_char_p
47 | self.ERR_error_string.argtypes = [ctypes.c_ulong, ctypes.c_char_p]
48 |
49 | self.ERR_get_error = self._lib.ERR_get_error
50 | self.ERR_get_error.restype = ctypes.c_ulong
51 | self.ERR_get_error.argtypes = []
52 |
53 | self.BN_new = self._lib.BN_new
54 | self.BN_new.restype = ctypes.c_void_p
55 | self.BN_new.argtypes = []
56 |
57 | self.BN_free = self._lib.BN_free
58 | self.BN_free.restype = None
59 | self.BN_free.argtypes = [ctypes.c_void_p]
60 |
61 | self.OPENSSL_free = self._lib.CRYPTO_free
62 | self.OPENSSL_free.restype = None
63 | self.OPENSSL_free.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int]
64 |
65 | self.BN_num_bits = self._lib.BN_num_bits
66 | self.BN_num_bits.restype = ctypes.c_int
67 | self.BN_num_bits.argtypes = [ctypes.c_void_p]
68 |
69 | self.BN_bn2bin = self._lib.BN_bn2bin
70 | self.BN_bn2bin.restype = ctypes.c_int
71 | self.BN_bn2bin.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
72 |
73 | self.BN_bin2bn = self._lib.BN_bin2bn
74 | self.BN_bin2bn.restype = ctypes.c_void_p
75 | self.BN_bin2bn.argtypes = [ctypes.c_void_p, ctypes.c_int,
76 | ctypes.c_void_p]
77 |
78 | self.EC_GROUP_get_degree = self._lib.EC_GROUP_get_degree
79 | self.EC_GROUP_get_degree.restype = ctypes.c_int
80 | self.EC_GROUP_get_degree.argtypes = [ctypes.c_void_p]
81 |
82 | self.EC_KEY_free = self._lib.EC_KEY_free
83 | self.EC_KEY_free.restype = None
84 | self.EC_KEY_free.argtypes = [ctypes.c_void_p]
85 |
86 | self.EC_KEY_new_by_curve_name = self._lib.EC_KEY_new_by_curve_name
87 | self.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p
88 | self.EC_KEY_new_by_curve_name.argtypes = [ctypes.c_int]
89 |
90 | self.EC_KEY_generate_key = self._lib.EC_KEY_generate_key
91 | self.EC_KEY_generate_key.restype = ctypes.c_int
92 | self.EC_KEY_generate_key.argtypes = [ctypes.c_void_p]
93 |
94 | self.EC_KEY_check_key = self._lib.EC_KEY_check_key
95 | self.EC_KEY_check_key.restype = ctypes.c_int
96 | self.EC_KEY_check_key.argtypes = [ctypes.c_void_p]
97 |
98 | self.EC_KEY_get0_private_key = self._lib.EC_KEY_get0_private_key
99 | self.EC_KEY_get0_private_key.restype = ctypes.c_void_p
100 | self.EC_KEY_get0_private_key.argtypes = [ctypes.c_void_p]
101 |
102 | self.EC_KEY_get0_public_key = self._lib.EC_KEY_get0_public_key
103 | self.EC_KEY_get0_public_key.restype = ctypes.c_void_p
104 | self.EC_KEY_get0_public_key.argtypes = [ctypes.c_void_p]
105 |
106 | self.EC_KEY_get0_group = self._lib.EC_KEY_get0_group
107 | self.EC_KEY_get0_group.restype = ctypes.c_void_p
108 | self.EC_KEY_get0_group.argtypes = [ctypes.c_void_p]
109 |
110 | self.EC_POINT_get_affine_coordinates_GFp = self._lib.EC_POINT_get_affine_coordinates_GFp
111 | self.EC_POINT_get_affine_coordinates_GFp.restype = ctypes.c_int
112 | self.EC_POINT_get_affine_coordinates_GFp.argtypes = 5 * [ctypes.c_void_p]
113 |
114 | self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key
115 | self.EC_KEY_set_private_key.restype = ctypes.c_int
116 | self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p,
117 | ctypes.c_void_p]
118 |
119 | self.EC_KEY_set_public_key = self._lib.EC_KEY_set_public_key
120 | self.EC_KEY_set_public_key.restype = ctypes.c_int
121 | self.EC_KEY_set_public_key.argtypes = [ctypes.c_void_p,
122 | ctypes.c_void_p]
123 |
124 | self.EC_KEY_set_group = self._lib.EC_KEY_set_group
125 | self.EC_KEY_set_group.restype = ctypes.c_int
126 | self.EC_KEY_set_group.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
127 |
128 | self.EC_POINT_set_affine_coordinates_GFp = self._lib.EC_POINT_set_affine_coordinates_GFp
129 | self.EC_POINT_set_affine_coordinates_GFp.restype = ctypes.c_int
130 | self.EC_POINT_set_affine_coordinates_GFp.argtypes = 5 * [ctypes.c_void_p]
131 |
132 | self.EC_POINT_new = self._lib.EC_POINT_new
133 | self.EC_POINT_new.restype = ctypes.c_void_p
134 | self.EC_POINT_new.argtypes = [ctypes.c_void_p]
135 |
136 | self.EC_POINT_free = self._lib.EC_POINT_free
137 | self.EC_POINT_free.restype = None
138 | self.EC_POINT_free.argtypes = [ctypes.c_void_p]
139 |
140 | self.BN_CTX_free = self._lib.BN_CTX_free
141 | self.BN_CTX_free.restype = None
142 | self.BN_CTX_free.argtypes = [ctypes.c_void_p]
143 |
144 | self.EC_POINT_mul = self._lib.EC_POINT_mul
145 | self.EC_POINT_mul.restype = ctypes.c_int
146 | self.EC_POINT_mul.argtypes = [ctypes.c_void_p, ctypes.c_void_p,
147 | ctypes.c_void_p, ctypes.c_void_p,
148 | ctypes.c_void_p, ctypes.c_void_p]
149 |
150 | self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key
151 | self.EC_KEY_set_private_key.restype = ctypes.c_int
152 | self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p,
153 | ctypes.c_void_p]
154 |
155 | # self.ECDH_OpenSSL = self._lib.ECDH_OpenSSL
156 | # self._lib.ECDH_OpenSSL.restype = ctypes.c_void_p
157 | # self._lib.ECDH_OpenSSL.argtypes = []
158 |
159 | self.BN_CTX_new = self._lib.BN_CTX_new
160 | self._lib.BN_CTX_new.restype = ctypes.c_void_p
161 | self._lib.BN_CTX_new.argtypes = []
162 |
163 | # self.ECDH_set_method = self._lib.ECDH_set_method
164 | # self._lib.ECDH_set_method.restype = ctypes.c_int
165 | # self._lib.ECDH_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
166 |
167 | self.ECDH_compute_key = self._lib.ECDH_compute_key
168 | self.ECDH_compute_key.restype = ctypes.c_int
169 | self.ECDH_compute_key.argtypes = [ctypes.c_void_p,
170 | ctypes.c_int,
171 | ctypes.c_void_p,
172 | ctypes.c_void_p,
173 | ctypes.c_void_p]
174 |
175 | self.EVP_CipherInit_ex = self._lib.EVP_CipherInit_ex
176 | self.EVP_CipherInit_ex.restype = ctypes.c_int
177 | self.EVP_CipherInit_ex.argtypes = [ctypes.c_void_p,
178 | ctypes.c_void_p, ctypes.c_void_p]
179 |
180 | self.EVP_CIPHER_CTX_new = self._lib.EVP_CIPHER_CTX_new
181 | self.EVP_CIPHER_CTX_new.restype = ctypes.c_void_p
182 | self.EVP_CIPHER_CTX_new.argtypes = []
183 |
184 | # Cipher
185 | self.EVP_aes_128_cfb128 = self._lib.EVP_aes_128_cfb128
186 | self.EVP_aes_128_cfb128.restype = ctypes.c_void_p
187 | self.EVP_aes_128_cfb128.argtypes = []
188 |
189 | self.EVP_aes_256_cfb128 = self._lib.EVP_aes_256_cfb128
190 | self.EVP_aes_256_cfb128.restype = ctypes.c_void_p
191 | self.EVP_aes_256_cfb128.argtypes = []
192 |
193 | self.EVP_aes_128_cbc = self._lib.EVP_aes_128_cbc
194 | self.EVP_aes_128_cbc.restype = ctypes.c_void_p
195 | self.EVP_aes_128_cbc.argtypes = []
196 |
197 | self.EVP_aes_256_cbc = self._lib.EVP_aes_256_cbc
198 | self.EVP_aes_256_cbc.restype = ctypes.c_void_p
199 | self.EVP_aes_256_cbc.argtypes = []
200 |
201 | try:
202 | self.EVP_aes_128_ctr = self._lib.EVP_aes_128_ctr
203 | except AttributeError:
204 | pass
205 | else:
206 | self.EVP_aes_128_ctr.restype = ctypes.c_void_p
207 | self.EVP_aes_128_ctr.argtypes = []
208 |
209 | try:
210 | self.EVP_aes_256_ctr = self._lib.EVP_aes_256_ctr
211 | except AttributeError:
212 | pass
213 | else:
214 | self.EVP_aes_256_ctr.restype = ctypes.c_void_p
215 | self.EVP_aes_256_ctr.argtypes = []
216 |
217 | self.EVP_aes_128_ofb = self._lib.EVP_aes_128_ofb
218 | self.EVP_aes_128_ofb.restype = ctypes.c_void_p
219 | self.EVP_aes_128_ofb.argtypes = []
220 |
221 | self.EVP_aes_256_ofb = self._lib.EVP_aes_256_ofb
222 | self.EVP_aes_256_ofb.restype = ctypes.c_void_p
223 | self.EVP_aes_256_ofb.argtypes = []
224 |
225 | self.EVP_bf_cbc = self._lib.EVP_bf_cbc
226 | self.EVP_bf_cbc.restype = ctypes.c_void_p
227 | self.EVP_bf_cbc.argtypes = []
228 |
229 | self.EVP_bf_cfb64 = self._lib.EVP_bf_cfb64
230 | self.EVP_bf_cfb64.restype = ctypes.c_void_p
231 | self.EVP_bf_cfb64.argtypes = []
232 |
233 | self.EVP_rc4 = self._lib.EVP_rc4
234 | self.EVP_rc4.restype = ctypes.c_void_p
235 | self.EVP_rc4.argtypes = []
236 |
237 | # self.EVP_CIPHER_CTX_cleanup = self._lib.EVP_CIPHER_CTX_cleanup
238 | # self.EVP_CIPHER_CTX_cleanup.restype = ctypes.c_int
239 | # self.EVP_CIPHER_CTX_cleanup.argtypes = [ctypes.c_void_p]
240 |
241 | self.EVP_CIPHER_CTX_free = self._lib.EVP_CIPHER_CTX_free
242 | self.EVP_CIPHER_CTX_free.restype = None
243 | self.EVP_CIPHER_CTX_free.argtypes = [ctypes.c_void_p]
244 |
245 | self.EVP_CipherUpdate = self._lib.EVP_CipherUpdate
246 | self.EVP_CipherUpdate.restype = ctypes.c_int
247 | self.EVP_CipherUpdate.argtypes = [ctypes.c_void_p,
248 | ctypes.c_void_p,
249 | ctypes.c_void_p,
250 | ctypes.c_void_p,
251 | ctypes.c_int]
252 |
253 | self.EVP_CipherFinal_ex = self._lib.EVP_CipherFinal_ex
254 | self.EVP_CipherFinal_ex.restype = ctypes.c_int
255 | self.EVP_CipherFinal_ex.argtypes = 3 * [ctypes.c_void_p]
256 |
257 | self.EVP_DigestInit = self._lib.EVP_DigestInit
258 | self.EVP_DigestInit.restype = ctypes.c_int
259 | self._lib.EVP_DigestInit.argtypes = 2 * [ctypes.c_void_p]
260 |
261 | self.EVP_DigestInit_ex = self._lib.EVP_DigestInit_ex
262 | self.EVP_DigestInit_ex.restype = ctypes.c_int
263 | self._lib.EVP_DigestInit_ex.argtypes = 3 * [ctypes.c_void_p]
264 |
265 | self.EVP_DigestUpdate = self._lib.EVP_DigestUpdate
266 | self.EVP_DigestUpdate.restype = ctypes.c_int
267 | self.EVP_DigestUpdate.argtypes = [ctypes.c_void_p,
268 | ctypes.c_void_p,
269 | ctypes.c_int]
270 |
271 | self.EVP_DigestFinal = self._lib.EVP_DigestFinal
272 | self.EVP_DigestFinal.restype = ctypes.c_int
273 | self.EVP_DigestFinal.argtypes = [ctypes.c_void_p,
274 | ctypes.c_void_p, ctypes.c_void_p]
275 |
276 | self.EVP_DigestFinal_ex = self._lib.EVP_DigestFinal_ex
277 | self.EVP_DigestFinal_ex.restype = ctypes.c_int
278 | self.EVP_DigestFinal_ex.argtypes = [ctypes.c_void_p,
279 | ctypes.c_void_p, ctypes.c_void_p]
280 |
281 | # self.EVP_ecdsa = self._lib.EVP_ecdsa
282 | # self._lib.EVP_ecdsa.restype = ctypes.c_void_p
283 | # self._lib.EVP_ecdsa.argtypes = []
284 |
285 | self.ECDSA_sign = self._lib.ECDSA_sign
286 | self.ECDSA_sign.restype = ctypes.c_int
287 | self.ECDSA_sign.argtypes = [ctypes.c_int,
288 | ctypes.c_void_p,
289 | ctypes.c_int,
290 | ctypes.c_void_p,
291 | ctypes.c_void_p,
292 | ctypes.c_void_p]
293 |
294 | self.ECDSA_verify = self._lib.ECDSA_verify
295 | self.ECDSA_verify.restype = ctypes.c_int
296 | self.ECDSA_verify.argtypes = [ctypes.c_int,
297 | ctypes.c_void_p,
298 | ctypes.c_int,
299 | ctypes.c_void_p,
300 | ctypes.c_int,
301 | ctypes.c_void_p]
302 |
303 | # self.EVP_MD_CTX_create = self._lib.EVP_MD_CTX_create
304 | # self.EVP_MD_CTX_create.restype = ctypes.c_void_p
305 | # self.EVP_MD_CTX_create.argtypes = []
306 |
307 | # self.EVP_MD_CTX_init = self._lib.EVP_MD_CTX_init
308 | # self.EVP_MD_CTX_init.restype = None
309 | # self.EVP_MD_CTX_init.argtypes = [ctypes.c_void_p]
310 |
311 | # self.EVP_MD_CTX_destroy = self._lib.EVP_MD_CTX_destroy
312 | # self.EVP_MD_CTX_destroy.restype = None
313 | # self.EVP_MD_CTX_destroy.argtypes = [ctypes.c_void_p]
314 |
315 | self.RAND_bytes = self._lib.RAND_bytes
316 | self.RAND_bytes.restype = ctypes.c_int
317 | self.RAND_bytes.argtypes = [ctypes.c_void_p, ctypes.c_int]
318 |
319 | self.EVP_sha256 = self._lib.EVP_sha256
320 | self.EVP_sha256.restype = ctypes.c_void_p
321 | self.EVP_sha256.argtypes = []
322 |
323 | self.i2o_ECPublicKey = self._lib.i2o_ECPublicKey
324 | self.i2o_ECPublicKey.restype = ctypes.c_int
325 | # self.i2o_ECPublicKey.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
326 | self.i2o_ECPublicKey.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.POINTER(ctypes.c_ubyte))]
327 |
328 | self.i2d_ECPrivateKey = self._lib.i2d_ECPrivateKey
329 | self.i2d_ECPrivateKey.restype = ctypes.c_int
330 | # self.i2d_ECPrivateKey.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
331 | self.i2d_ECPrivateKey.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.POINTER(ctypes.c_ubyte))]
332 |
333 | self.o2i_ECPublicKey = self._lib.o2i_ECPublicKey
334 | self.o2i_ECPublicKey.restype = ctypes.c_void_p
335 | self.o2i_ECPublicKey.argtypes = [ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_char_p), ctypes.c_long]
336 |
337 | self.d2i_ECPrivateKey = self._lib.d2i_ECPrivateKey
338 | self.d2i_ECPrivateKey.restype = ctypes.c_void_p
339 | self.d2i_ECPrivateKey.argtypes = [ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_char_p), ctypes.c_long]
340 |
341 | self.EVP_sha512 = self._lib.EVP_sha512
342 | self.EVP_sha512.restype = ctypes.c_void_p
343 | self.EVP_sha512.argtypes = []
344 |
345 | self.HMAC = self._lib.HMAC
346 | self.HMAC.restype = ctypes.c_void_p
347 | self.HMAC.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int,
348 | ctypes.c_void_p, ctypes.c_int,
349 | ctypes.c_void_p, ctypes.c_void_p]
350 |
351 | try:
352 | self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC
353 | except:
354 | # The above is not compatible with all versions of OSX.
355 | self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC_SHA1
356 | self.PKCS5_PBKDF2_HMAC.restype = ctypes.c_int
357 | self.PKCS5_PBKDF2_HMAC.argtypes = [ctypes.c_void_p, ctypes.c_int,
358 | ctypes.c_void_p, ctypes.c_int,
359 | ctypes.c_int, ctypes.c_void_p,
360 | ctypes.c_int, ctypes.c_void_p]
361 |
362 | self._set_ciphers()
363 | self._set_curves()
364 |
365 | def _set_ciphers(self):
366 | self.cipher_algo = {
367 | 'aes-128-cbc': CipherName('aes-128-cbc',
368 | self.EVP_aes_128_cbc,
369 | 16),
370 | 'aes-256-cbc': CipherName('aes-256-cbc',
371 | self.EVP_aes_256_cbc,
372 | 16),
373 | 'aes-128-cfb': CipherName('aes-128-cfb',
374 | self.EVP_aes_128_cfb128,
375 | 16),
376 | 'aes-256-cfb': CipherName('aes-256-cfb',
377 | self.EVP_aes_256_cfb128,
378 | 16),
379 | 'aes-128-ofb': CipherName('aes-128-ofb',
380 | self._lib.EVP_aes_128_ofb,
381 | 16),
382 | 'aes-256-ofb': CipherName('aes-256-ofb',
383 | self._lib.EVP_aes_256_ofb,
384 | 16),
385 | # 'aes-128-ctr': CipherName('aes-128-ctr',
386 | # self._lib.EVP_aes_128_ctr,
387 | # 16),
388 | # 'aes-256-ctr': CipherName('aes-256-ctr',
389 | # self._lib.EVP_aes_256_ctr,
390 | # 16),
391 | 'bf-cfb': CipherName('bf-cfb',
392 | self.EVP_bf_cfb64,
393 | 8),
394 | 'bf-cbc': CipherName('bf-cbc',
395 | self.EVP_bf_cbc,
396 | 8),
397 | 'rc4': CipherName('rc4',
398 | self.EVP_rc4,
399 | # 128 is the initialisation size not block size
400 | 128),
401 | }
402 |
403 | if hasattr(self, 'EVP_aes_128_ctr'):
404 | self.cipher_algo['aes-128-ctr'] = CipherName(
405 | 'aes-128-ctr',
406 | self._lib.EVP_aes_128_ctr,
407 | 16
408 | )
409 | if hasattr(self, 'EVP_aes_256_ctr'):
410 | self.cipher_algo['aes-256-ctr'] = CipherName(
411 | 'aes-256-ctr',
412 | self._lib.EVP_aes_256_ctr,
413 | 16
414 | )
415 |
416 | def _set_curves(self):
417 | self.curves = {
418 | 'secp112r1': 704,
419 | 'secp112r2': 705,
420 | 'secp128r1': 706,
421 | 'secp128r2': 707,
422 | 'secp160k1': 708,
423 | 'secp160r1': 709,
424 | 'secp160r2': 710,
425 | 'secp192k1': 711,
426 | 'secp224k1': 712,
427 | 'secp224r1': 713,
428 | 'secp256k1': 714,
429 | 'secp384r1': 715,
430 | 'secp521r1': 716,
431 | 'sect113r1': 717,
432 | 'sect113r2': 718,
433 | 'sect131r1': 719,
434 | 'sect131r2': 720,
435 | 'sect163k1': 721,
436 | 'sect163r1': 722,
437 | 'sect163r2': 723,
438 | 'sect193r1': 724,
439 | 'sect193r2': 725,
440 | 'sect233k1': 726,
441 | 'sect233r1': 727,
442 | 'sect239k1': 728,
443 | 'sect283k1': 729,
444 | 'sect283r1': 730,
445 | 'sect409k1': 731,
446 | 'sect409r1': 732,
447 | 'sect571k1': 733,
448 | 'sect571r1': 734,
449 | 'prime256v1': 415,
450 | }
451 |
452 | def BN_num_bytes(self, x):
453 | """
454 | returns the length of a BN (OpenSSl API)
455 | """
456 | return int((self.BN_num_bits(x) + 7) / 8)
457 |
458 | def get_cipher(self, name):
459 | """
460 | returns the OpenSSL cipher instance
461 | """
462 | if name not in self.cipher_algo:
463 | raise Exception("Unknown cipher")
464 | return self.cipher_algo[name]
465 |
466 | def get_curve(self, name):
467 | """
468 | returns the id of a elliptic curve
469 | """
470 | if name not in self.curves:
471 | raise Exception("Unknown curve")
472 | return self.curves[name]
473 |
474 | def get_curve_by_id(self, id):
475 | """
476 | returns the name of a elliptic curve with his id
477 | """
478 | res = None
479 | for i in self.curves:
480 | if self.curves[i] == id:
481 | res = i
482 | break
483 | if res is None:
484 | raise Exception("Unknown curve")
485 | return res
486 |
487 | def rand(self, size):
488 | """
489 | OpenSSL random function
490 | """
491 | buffer = self.malloc(0, size)
492 | if self.RAND_bytes(buffer, size) != 1:
493 | raise RuntimeError("OpenSSL RAND_bytes failed")
494 | return buffer.raw
495 |
496 | def malloc(self, data, size):
497 | """
498 | returns a create_string_buffer (ctypes)
499 | """
500 | buffer = None
501 | if data != 0:
502 | if sys.version_info.major == 3 and isinstance(data, type('')):
503 | data = data.encode()
504 | buffer = self.create_string_buffer(data, size)
505 | else:
506 | buffer = self.create_string_buffer(size)
507 | return buffer
508 |
509 | def get_error(self):
510 | return OpenSSL.ERR_error_string(OpenSSL.ERR_get_error(), None)
511 |
512 | # libname = ctypes.util.find_library('crypto')
513 |
514 | def load_opensll_dll():
515 | # path = os.getcwd() + '/libeay32.dll'
516 | path = os.getcwd()
517 | if platform.architecture()[0] == '64bit':
518 | path += '/microchat/ecdh/x64/libeay32.dll'
519 | else:
520 | path += '/microchat/ecdh/x86/libeay32.dll'
521 | libname = ctypes.util.find_library(path)
522 | if libname is None:
523 | raise Exception("Couldn't load OpenSSL lib ...")
524 | return libname
525 |
526 | OpenSSL = _OpenSSL(load_opensll_dll())
527 |
--------------------------------------------------------------------------------
/microchat/ecdh/x64/libeay32.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LSilent/PyMicroChat/e2862b3766f52b924d13f7a75000271dfe1e3d76/microchat/ecdh/x64/libeay32.dll
--------------------------------------------------------------------------------
/microchat/ecdh/x86/libeay32.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LSilent/PyMicroChat/e2862b3766f52b924d13f7a75000271dfe1e3d76/microchat/ecdh/x86/libeay32.dll
--------------------------------------------------------------------------------
/microchat/interface.py:
--------------------------------------------------------------------------------
1 | import http.client
2 | import logging
3 | import time
4 | import random
5 | import struct
6 | import string
7 | import urllib
8 | import xmltodict
9 | import zlib
10 | from . import define
11 | from . import dns_ip
12 | from . import business
13 | from . import Util
14 | from . import mm_pb2
15 | from .plugin.logger_wrapper import logger, ColorDefine
16 | from google.protobuf.internal import decoder, encoder
17 | from . import logo_bingo
18 |
19 |
20 | # 登录,参数为账号,密码
21 | def Login(name, password):
22 | # 组包
23 | (senddata, login_aes_key) = business.login_req2buf(name, password)
24 | # 发包
25 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/manualauth', senddata)
26 | logger.debug('返回数据:' + str(ret_bytes))
27 | # 解包
28 | return business.login_buf2Resp(ret_bytes, login_aes_key)
29 |
30 | # 首次登录设备初始化
31 | def new_init():
32 | continue_flag = True # 需要继续初始化标志位(联系人过多需要多次初始化才能获取完整好友列表)
33 | cur = max = b''
34 | while continue_flag:
35 | # 组包
36 | send_data = business.new_init_req2buf(cur, max)
37 | # 发包
38 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/newinit', send_data)
39 | logger.debug('new_init返回数据:' + str(ret_bytes))
40 | # 解包
41 | (continue_flag, cur, max) = business.new_init_buf2resp(ret_bytes)
42 |
43 | return
44 |
45 | # 同步消息
46 | def new_sync():
47 | # 组包
48 | send_data = business.new_sync_req2buf()
49 | # 发包
50 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/newsync', send_data)
51 | logger.debug('new_sync返回数据:' + str(ret_bytes))
52 | # 解包
53 | return business.new_sync_buf2resp(ret_bytes)
54 |
55 | # 发消息(Utf-8编码)(使用at功能时消息内容必须至少有相同数量的@符号,允许不以\u2005结尾)
56 | def new_send_msg(to_wxid, msg_content, at_user_list = [], msg_type=1):
57 | # 组包
58 | send_data = business.new_send_msg_req2buf(to_wxid, msg_content, at_user_list, msg_type)
59 | # 发包
60 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/newsendmsg', send_data)
61 | logger.debug('new_send_msg返回数据:' + Util.b2hex(ret_bytes))
62 | # 解包
63 | (ret_code, svrid) = business.new_send_msg_buf2resp(ret_bytes)
64 | # 消息记录存入数据库
65 | Util.insert_msg_to_db(svrid, Util.get_utc(), Util.wxid,to_wxid, msg_type, msg_content.decode())
66 | # 返回发送消息结果
67 | return ret_code, svrid
68 |
69 | # 分享链接
70 | def send_app_msg(to_wxid, title, des, link_url, thumb_url=''):
71 | # 组包
72 | send_data, msg_content, client_msg_id = business.send_app_msg_req2buf(to_wxid, title, des, link_url, thumb_url)
73 | # 发包
74 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/sendappmsg', send_data)
75 | logger.debug('send_app_msg返回数据:' + Util.b2hex(ret_bytes))
76 | # 解包
77 | (ret_code, svrid) = business.send_app_msg_buf2resp(ret_bytes)
78 | # 消息记录存入数据库
79 | Util.insert_msg_to_db(svrid, Util.get_utc(),Util.wxid, to_wxid, 5, msg_content, client_msg_id)
80 | # 返回发送消息结果
81 | return ret_code, svrid
82 |
83 | # 获取好友列表(wxid,昵称,备注,alias,v1_name,头像)
84 | def get_contact_list(contact_type=Util.CONTACT_TYPE_FRIEND):
85 | return Util.get_contact(contact_type)
86 |
87 | # 好友请求
88 | def verify_user(opcode, user_wxid, user_v1_name, user_ticket, user_anti_ticket, send_content):
89 | # 组包
90 | send_data = business.verify_user_req2buf(opcode, user_wxid, user_v1_name, user_ticket, user_anti_ticket, send_content)
91 | # 发包
92 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/verifyuser', send_data)
93 | logger.debug('verify_user返回数据:' + Util.b2hex(ret_bytes))
94 | # 解包
95 | return business.verify_user_msg_buf2resp(ret_bytes)
96 |
97 | # 收红包(返回0表示领取成功)
98 | def receive_and_open_wxhb(channelId,msgType,nativeUrl,sendId,inWay = 1,ver='v1.0'):
99 | # 请求timingIdentifier组包
100 | send_data = business.recieve_wxhb_req2buf(channelId,msgType,nativeUrl,sendId,inWay,ver)
101 | # 请求timingIdentifier发包
102 | ret_bytes = Util.mmPost('/cgi-bin/mmpay-bin/receivewxhb', send_data)
103 | logger.debug('receivewxhb返回数据:' + Util.b2hex(ret_bytes))
104 | # 请求timingIdentifier解包
105 | (timingIdentifier,sessionUserName) = business.recieve_wxhb_buf2resp(ret_bytes)
106 | if timingIdentifier and sessionUserName:
107 | # 拆红包组包
108 | send_data = business.open_wxhb_req2buf(channelId,msgType,nativeUrl,sendId,sessionUserName,timingIdentifier,ver)
109 | # 拆红包发包
110 | ret_bytes = Util.mmPost('/cgi-bin/mmpay-bin/openwxhb', send_data)
111 | logger.debug('openwxhb返回数据:' + Util.b2hex(ret_bytes))
112 | # 拆红包解包
113 | return business.open_wxhb_buf2resp(ret_bytes)
114 | return (-1,'')
115 |
116 | # 查看红包详情(limit,offset参数设置本次请求返回领取红包人数区间)
117 | def qry_detail_wxhb(nativeUrl, sendId, limit = 11, offset = 0, ver='v1.0'):
118 | # 组包
119 | send_data = business.qry_detail_wxhb_req2buf(nativeUrl, sendId, limit, offset, ver)
120 | # 发包
121 | ret_bytes = Util.mmPost('/cgi-bin/mmpay-bin/qrydetailwxhb', send_data)
122 | logger.debug('qrydetailwxhb返回数据:' + Util.b2hex(ret_bytes))
123 | # 解包
124 | return business.qry_detail_wxhb_buf2resp(ret_bytes)
125 |
126 | # 发emoji表情:file_name为emoji加密文件名; game_type=0直接发送emoji; game_type=1无视file_name参数,接收方播放石头剪刀布动画;其余game_type值均为投骰子动画;
127 | # content只在game_type不为0即发送游戏表情时有效;content取1-3代表剪刀、石头、布;content取4-9代表投骰子1-6点;
128 | def send_emoji(wxid, file_name, game_type, content):
129 | # 组包
130 | send_data, client_msg_id = business.send_emoji_req2buf(wxid, file_name, game_type, content)
131 | # 发包
132 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/sendemoji', send_data)
133 | logger.debug('send_emoji返回数据:' + Util.b2hex(ret_bytes))
134 | # 解包
135 | ret_code, svrid = business.send_emoji_buf2resp(ret_bytes)
136 | # 消息记录存入数据库
137 | Util.insert_msg_to_db(svrid, Util.get_utc(), Util.wxid, wxid, 47, file_name, client_msg_id)
138 | return ret_code, svrid
139 |
140 | # 收款
141 | def transfer_operation(invalid_time, trans_id, transaction_id, user_name):
142 | # 组包
143 | send_data = business.transfer_operation_req2buf(invalid_time, trans_id, transaction_id, user_name)
144 | # 发包
145 | ret_bytes = Util.mmPost('/cgi-bin/mmpay-bin/transferoperation', send_data)
146 | logger.debug('transfer_operation返回数据:' + Util.b2hex(ret_bytes))
147 | # 解包
148 | return business.transfer_operation_buf2resp(ret_bytes)
149 |
150 | # 查询转账结果
151 | def transfer_query(invalid_time, trans_id, transfer_id):
152 | # 组包
153 | send_data = business.transfer_query_req2buf(invalid_time, trans_id, transfer_id)
154 | # 发包
155 | ret_bytes = Util.mmPost('/cgi-bin/mmpay-bin/transferquery', send_data)
156 | logger.debug('transfer_query返回数据:' + Util.b2hex(ret_bytes))
157 | # 解包
158 | return business.transfer_query_buf2resp(ret_bytes)
159 |
160 | # 刷新好友信息(通讯录中的好友或群聊可以获取详细信息;陌生人仅可获取昵称和头像)
161 | def get_contact(wxid):
162 | # 组包
163 | send_data = business.get_contact_req2buf(wxid)
164 | # 发包
165 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/getcontact', send_data)
166 | logger.debug('get_contact返回数据:' + Util.b2hex(ret_bytes))
167 | # 解包
168 | return business.get_contact_buf2resp(ret_bytes)
169 |
170 | # 获取群成员列表
171 | def get_chatroom_member_list(wxid):
172 | friend, __ = get_contact(wxid)
173 | return [member.wxid for member in friend.group_member_list.member]
174 |
175 | # 建群聊(参数group_member_list为群成员wxid)(建群成功返回新建群聊的wxid)
176 | def create_chatroom(group_member_list):
177 | # 组包
178 | send_data = business.create_chatroom_req2buf(group_member_list)
179 | # 发包
180 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/createchatroom', send_data)
181 | logger.debug('createchatroom返回数据:' + Util.b2hex(ret_bytes))
182 | # 解包
183 | return business.create_chatroom_buf2resp(ret_bytes)
184 |
185 | # 面对面建群(参数为群密码,建群地点经纬度)(无需好友建群方便测试)
186 | # 建群成功返回新建群聊的wxid;系统会发10000消息通知群创建成功;使用相同密码短时间内会返回同一群聊
187 | def mm_facing_create_chatroom(pwd = '9999', lon = 116.39, lat = 38.90):
188 | # 面对面建群步骤1组包
189 | send_data = business.mm_facing_create_chatroom_req2buf(0, pwd, lon, lat)
190 | # 面对面建群步骤1发包
191 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/mmfacingcreatechatroom', send_data)
192 | logger.debug('mmfacingcreatechatroom返回数据:' + Util.b2hex(ret_bytes))
193 | # 面对面建群步骤1解包
194 | ret, wxid = business.mm_facing_create_chatroom_buf2resp(ret_bytes, 0)
195 | if not ret:
196 | # 面对面建群步骤2组包
197 | send_data = business.mm_facing_create_chatroom_req2buf(1, pwd, lon, lat)
198 | # 面对面建群步骤2发包
199 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/mmfacingcreatechatroom', send_data)
200 | logger.debug('mmfacingcreatechatroom返回数据:' + Util.b2hex(ret_bytes))
201 | # 面对面建群步骤2解包
202 | ret, wxid = business.mm_facing_create_chatroom_buf2resp(ret_bytes, 1)
203 | return wxid
204 | return ''
205 |
206 | # 群聊拉人
207 | def add_chatroom_member(chatroom_wxid, member_list):
208 | # 组包
209 | send_data = business.add_chatroom_member_req2buf(chatroom_wxid, member_list)
210 | # 发包
211 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/addchatroommember', send_data)
212 | logger.debug('addchatroommember返回数据:' + Util.b2hex(ret_bytes))
213 | # 解包
214 | return business.add_chatroom_member_buf2resp(ret_bytes)
215 |
216 | # 群聊中at所有人(每100人一条消息)(最后发送文字消息)
217 | def at_all_in_group(chatroom_wxid, send_text):
218 | group, __ = get_contact(chatroom_wxid)
219 | at_text = ''
220 | at_list = []
221 | for i in range(group.group_member_list.cnt):
222 | if i and i%100 == 0:
223 | #每at100人发送一次消息
224 | new_send_msg(chatroom_wxid,at_text.encode(encoding = 'utf-8'), at_list)
225 | at_text = ''
226 | at_list = []
227 | at_text += '@{}'.format(group.group_member_list.member[i].nick_name)
228 | at_list.append(group.group_member_list.member[i].wxid)
229 | if at_text and at_list:
230 | new_send_msg(chatroom_wxid,at_text.encode(encoding = 'utf-8'), at_list)
231 | # 发送文字消息
232 | if send_text:
233 | new_send_msg(chatroom_wxid,send_text.encode(encoding = 'utf-8'))
234 | return
235 |
236 | # 设置群聊中自己昵称(utf-8)
237 | def set_group_nick_name(chatroom_wxid, nick_name):
238 | # 组包
239 | send_data = business.set_group_nick_name_req2buf(chatroom_wxid, nick_name)
240 | # 发包
241 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/oplog', send_data)
242 | logger.debug('oplog返回数据:' + Util.b2hex(ret_bytes))
243 | # 解包
244 | return business.set_group_nick_name_buf2resp(ret_bytes)
245 |
246 | # 消息撤回
247 | def revoke_msg(wxid, svrid):
248 | # 组包
249 | send_data = business.revoke_msg_req2buf(wxid, svrid)
250 | # 发包
251 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/revokemsg', send_data)
252 | logger.debug('revokemsg返回数据:' + Util.b2hex(ret_bytes))
253 | # 解包
254 | return business.revoke_msg_buf2resp(ret_bytes)
255 |
256 | # 从通讯录中删除好友/恢复好友(删除对方后可以用此接口再添加对方)
257 | # 群聊使用此接口可以保存到通讯录
258 | def delete_friend(wxid, delete = True):
259 | # 获取好友(群聊)详细信息
260 | friend, __ = get_contact(wxid)
261 | # 设置保存通讯录标志位
262 | if delete:
263 | friend.type = (friend.type>>1)<<1 # 清零
264 | else:
265 | friend.type |= 1 # 置1
266 | # 组包
267 | send_data = business.op_friend_req2buf(friend.SerializeToString())
268 | # 发包
269 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/oplog', send_data)
270 | logger.debug('oplog返回数据:' + Util.b2hex(ret_bytes))
271 | # 解包
272 | return business.op_friend_buf2resp(ret_bytes)
273 |
274 | # 拉黑/恢复 好友关系
275 | def ban_friend(wxid, ban = True):
276 | # 获取好友(群聊)详细信息
277 | friend, __ = get_contact(wxid)
278 | # 设置黑名单标志位
279 | if ban:
280 | friend.type |= 1<<3 # 置1
281 | else:
282 | friend.type = ((friend.type>>4)<<4) + (friend.type&7) # 清零
283 | # 组包
284 | send_data = business.op_friend_req2buf(friend.SerializeToString())
285 | # 发包
286 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/oplog', send_data)
287 | logger.debug('oplog返回数据:' + Util.b2hex(ret_bytes))
288 | # 解包
289 | return business.op_friend_buf2resp(ret_bytes)
290 |
291 | # 设置好友备注名/群聊名
292 | def set_friend_name(wxid, name):
293 | cmd_id = 2
294 | # 获取好友(群聊)详细信息
295 | friend, __ = get_contact(wxid)
296 | if wxid.endswith('@chatroom'):
297 | # 群聊: 设置群聊名/cmd_id
298 | friend.nickname.name = name
299 | cmd_id = 27
300 | else:
301 | # 好友: 设置备注名
302 | friend.remark_name.name = name
303 | # 组包
304 | send_data = business.op_friend_req2buf(friend.SerializeToString(), cmd_id)
305 | # 发包
306 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/oplog', send_data)
307 | logger.debug('oplog返回数据:' + Util.b2hex(ret_bytes))
308 | # 解包
309 | return business.op_friend_buf2resp(ret_bytes)
310 |
311 | # 发布群公告(仅限群主;自动@所有人)
312 | def set_chatroom_announcement(wxid, text):
313 | # 组包
314 | send_data = business.set_chatroom_announcement_req2buf(wxid, text)
315 | # 发包
316 | ret_bytes = Util.mmPost('/cgi-bin/micromsg-bin/setchatroomannouncement', send_data)
317 | logger.debug('setchatroomannouncement返回数据:' + Util.b2hex(ret_bytes))
318 | # 解包
319 | return business.set_chatroom_announcement_buf2resp(ret_bytes)
320 |
321 | # 初始化python模块
322 | def init_all():
323 | #配置logger
324 | logger.config("microchat", out=2)
325 | logo_bingo()
326 | # 初始化ECC key
327 | if not Util.GenEcdhKey():
328 | logger.error('初始化ECC Key失败!')
329 | Util.ExitProcess()
330 | # 从db加载dns
331 | dns_ip.load_dns()
332 |
--------------------------------------------------------------------------------
/microchat/mm.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto2";
2 |
3 | message mmStr
4 | {
5 | optional int32 len = 1;
6 | optional string str = 2;
7 | }
8 |
9 | message mmRes
10 | {
11 | optional int32 code = 1; //错误码
12 | optional string message = 2; //错误信息
13 | }
14 |
15 | message LoginInfo
16 | {
17 | optional bytes aesKey = 1; //解密服务器返回封包时使用的aeskey
18 | optional int32 uin = 2;
19 | optional string guid = 3; //手机guid长度16,这里取前15字节,以'\0'结尾
20 | optional int32 clientVer = 4;
21 | optional string androidVer = 5;
22 | optional int32 unknown = 6;
23 | }
24 |
25 | //同步key(该数据通常不需要解析,每次同步消息服务器会返回最新的完整的synckey,二进制保存即可;下次同步消息时直接使用)
26 | message SyncKey
27 | {
28 | optional int32 len = 1; //MsgKey总长度
29 | message MsgKey
30 | {
31 | optional int32 cnt = 1; //消息类型数量
32 | message Key
33 | {
34 | optional int32 type = 1; //消息类型
35 | optional int64 key = 2; //服务器消息id(通常每个消息加1,但不是从0开始)
36 | }
37 | repeated Key key = 2; //每种消息类型都有单独的synckey
38 | }
39 | optional MsgKey msgkey =2; //synckey
40 | }
41 |
42 | //同步消息
43 | message common_msg
44 | {
45 | optional int32 type = 1; //消息类型:1==>个人信息,2==>好友信息,5==>服务器上未读取的最新消息,其余消息类型暂未知
46 | message Data
47 | {
48 | optional int32 len = 1;
49 | optional bytes data = 2;
50 | }
51 | optional Data data = 2;
52 | }
53 |
54 | //wxid
55 | message Wxid
56 | {
57 | optional string id = 1;
58 | }
59 |
60 | //服务器返回消息(newinit/newsync)
61 | message Msg
62 | {
63 | optional int64 serverid = 1;
64 | optional Wxid from_id = 2; //发送方wxid
65 | optional Wxid to_id = 3; //接收方wxid
66 | optional int32 type = 4; //消息类型:9999==>系统垃圾消息,10002==>sysmsg(系统垃圾消息),49==>appmsg,1==>文字消息,10000==>系统提示
67 | message RawContent
68 | {
69 | optional string content = 1;
70 | }
71 | optional RawContent raw = 5; //原始消息内容,需要根据不同消息类型解析
72 | optional int32 status = 6;
73 | optional int32 tag7 = 7;
74 | optional bytes tag8 = 8;
75 | optional int32 createTime = 9; //消息发送时间
76 | optional string ex_info = 10; //消息附加内容(群是否屏蔽,群人数,群at功能)
77 | optional string xmlContent = 11; //推送内容(xml格式)
78 | optional int64 svrId = 12; //每条消息的唯一id
79 | optional int32 msgKey = 13; //sync key中的id
80 | }
81 |
82 | //好友详细信息
83 | message contact_info
84 | {
85 | optional Wxid wxid = 1;
86 | message NickName
87 | {
88 | optional string name = 1;
89 | }
90 | optional NickName nickname = 2;
91 | message PY_SHORT
92 | {
93 | optional string name = 1;
94 | }
95 | optional PY_SHORT shortPy = 3;
96 | message QuanPin
97 | {
98 | optional string name = 1;
99 | }
100 | optional QuanPin quanpin = 4;
101 | optional int32 sex = 5; //性别:0=>未知,1=>男,2=>女
102 | optional string tag6 = 6;
103 | optional int32 tag7 = 7;
104 | optional int32 type = 8; //好友状态:
105 | optional int32 tag9 = 9;
106 | message BeiZhu
107 | {
108 | optional string name = 1;
109 | }
110 | optional BeiZhu remark_name = 10; //备注名:为空则显示nickname
111 | message REAL_PY_SHORT
112 | {
113 | optional string name = 1;
114 | }
115 | optional REAL_PY_SHORT real_shortPy = 11;
116 | message REAL_QuanPin
117 | {
118 | optional string name = 1;
119 | }
120 | optional REAL_QuanPin real_quanpin = 12;
121 | optional int32 tag13 = 13;
122 | optional int32 tag14 = 14;
123 | optional string tag16 = 16;
124 | optional int32 bChatRoom = 17; //todo
125 | optional int32 tag18 = 18;
126 | optional string sheng = 19;
127 | optional string shi = 20;
128 | optional string qianming = 21; //签名
129 | optional int32 tag22 = 22; //todo
130 | optional int32 tag23 = 23; //todo
131 | optional int32 register_body_type = 24; //8=>个人;24=>公司
132 | optional string register_body = 25; //注册主体:xx公司或个人
133 | optional int32 tag26 = 26;
134 | optional int32 src = 27; //好友来源:(10000XX为对方添加自己)0=>未知;1=>QQ;3=>微信号;6=>名片;13=>手机通讯录;14=>群聊;15=>手机号;30=>扫一扫 (1000015=>对方手机号;1000030=>对方扫一扫;1000014=>对方群聊......)
135 |
136 | optional string lastMsgTime = 29;
137 | optional string alias = 30; //微信号(未设置则显示wxid)
138 |
139 | optional string chatroomOwnerWxid = 31;
140 |
141 | optional int32 tag33 = 33;
142 | optional int32 tag34 = 34;
143 | optional int32 tag35 = 35;
144 |
145 | optional bytes tag37 = 37; //个人相册封面图片
146 | optional string country = 38;
147 | optional string avatar_big = 39;
148 | optional string avatar_small= 40;
149 |
150 | optional bytes tag42 = 42;
151 |
152 | optional string v1_name = 45; //encryptName(等价于wxid,固定不变)
153 |
154 | optional bytes tag50 = 50;
155 |
156 | optional int32 chatroom_serverVer = 53;
157 | optional int32 chatroom_max_member = 55;
158 | optional int32 tag56 = 56;
159 | message GroupMemberList
160 | {
161 | optional int32 cnt = 1;
162 | message MemberInfo
163 | {
164 | optional string wxid = 1;
165 | optional string nick_name = 2; //昵称(非群昵称)
166 | optional int32 tag6 = 6;
167 | optional string inviteer_wxid = 7; //邀请入群wxid
168 | optional int32 tag8 = 8;
169 | }
170 | repeated MemberInfo member = 2;
171 | optional int32 tag3 = 3;
172 | optional string tag4 = 4;
173 | }
174 | optional GroupMemberList group_member_list = 57;
175 | optional int32 tag58 = 58;
176 |
177 | optional bytes tag62 = 62;
178 |
179 | optional int32 tag64 = 64;
180 | optional int32 tag65 = 65;
181 | optional int32 tag66 = 66;
182 | optional int32 tag67 = 67;
183 | }
184 |
185 | ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
186 |
187 |
188 | //登录请求--账号信息
189 | message ManualAuthAccountRequest
190 | {
191 | message AesKey
192 | {
193 | required int32 len = 1;
194 | required bytes key = 2;
195 | }
196 | required AesKey aes = 1; //仅用于本次登录请求,后续通讯使用的aeskey根据服务器返回的数据做ECDH生成
197 |
198 | message Ecdh
199 | {
200 | required int32 nid = 1; //椭圆曲线类型
201 |
202 | message EcdhKey
203 | {
204 | required int32 len = 1;
205 | required bytes key = 2; //椭圆曲线client pubkey
206 | }
207 | required EcdhKey ecdhKey = 2;
208 | }
209 | required Ecdh ecdh = 2;
210 | required string userName = 3;
211 | required string password1 = 4;
212 | required string password2 = 5;
213 | }
214 |
215 |
216 | //登录请求--设备信息
217 | message ManualAuthDeviceRequest
218 | {
219 | optional LoginInfo login = 1;
220 | message _Tag2
221 | {
222 | optional mmStr tag1 = 1;
223 | message __Tag2
224 | {
225 | optional string tag1 = 1;
226 | optional string tag2 = 2;
227 | optional string tag3 = 3;
228 | optional mmStr tag4 = 4;
229 | }
230 | optional __Tag2 tag2 = 2;
231 | message TAG3
232 | {
233 | optional string tag1 = 1;
234 | optional string tag2 = 2;
235 | }
236 | optional TAG3 tag3 = 3;
237 | optional mmStr tag4 = 4;
238 | optional mmStr tag5 = 5;
239 | optional int32 tag6 = 6;
240 | }
241 | optional _Tag2 tag2 = 2;
242 | optional string imei = 3;
243 | optional string softInfoXml = 4;
244 | optional int32 unknown5 = 5;
245 | optional string clientSeqID = 6;
246 | optional string clientSeqID_sign = 7;
247 | optional string loginDeviceName = 8;
248 | optional string deviceInfoXml = 9;
249 | optional string language = 10;
250 | optional string timeZone = 11;
251 | optional int32 unknown13 = 13;
252 | optional int32 unknown14 = 14;
253 | optional string deviceBrand = 15;
254 | optional string deviceModel = 16;
255 | optional string osType = 17;
256 | optional string realCountry = 18;
257 | optional int32 unknown22 = 22;
258 | }
259 |
260 | //登录结果
261 | message ManualAuthResponse
262 | {
263 | message AuthResult
264 | {
265 | required int32 code = 1; //登录错误码
266 | message ErrMsg
267 | {
268 | optional string msg = 1; //错误信息
269 | }
270 | required ErrMsg err_msg =2;
271 | }
272 | required AuthResult result = 1; //登录结果
273 | required int32 unifyFlag = 2;
274 | message AuthParam
275 | {
276 | required int32 uin = 1;
277 |
278 | message Ecdh
279 | {
280 | required int32 nid = 1;
281 | message EcdhKey
282 | {
283 | required int32 len = 1;
284 | optional bytes key = 2; //椭圆曲线server pubkey
285 | }
286 | optional EcdhKey ecdhKey = 2;
287 | }
288 | required Ecdh ecdh = 2;
289 |
290 | message SessionKey
291 | {
292 | required int32 len = 1;
293 | optional bytes key = 2;
294 | }
295 | required SessionKey session = 3; //加密的sessionKey 需要使用ECDH握手后的密钥做AES解密得到最终长16字节的aeskey
296 | optional bytes SmsTicket = 16; //需要短信授权时的ticket,用于后续请求验证码以及发送验证码
297 |
298 | optional string bindMailLoginUrl = 20;
299 | optional int32 serverTime = 22;
300 | }
301 | required AuthParam authParam = 3;
302 | message AccountInfo
303 | {
304 | optional string wxId = 1;
305 | optional string nickName = 2;
306 | optional int32 tag3 = 3;
307 | optional string bindMail = 4;
308 | optional string bindMobile = 5;
309 | optional string Alias = 6;
310 | optional string tag7 = 7;
311 | optional int32 status = 8;
312 | optional int32 pluginFlag = 9;
313 | optional int32 registerType = 10;
314 | optional string tag11 = 11;
315 | optional int32 safeDevice = 12;
316 | optional string officialNamePinyin = 13;
317 | optional string officialNameZh = 14;
318 | optional string tag15 = 15;
319 | optional string fsUrl = 16;
320 | }
321 | optional AccountInfo accountInfo = 4; //登录成功后返回账号信息
322 |
323 | message dns_info
324 | {
325 | message redirect_info
326 | {
327 | optional int32 cnt = 1; //host cnt
328 | message real_host_info
329 | {
330 | optional string host = 1; //host
331 | optional string redirect = 2; //redirect_host
332 | }
333 | repeated real_host_info real_host = 2;
334 | }
335 | optional redirect_info redirect = 1; //域名重定向信息
336 | message ip_info
337 | {
338 | optional int32 longlink_ip_cnt = 1; //长链接ip池数量
339 | optional int32 shortlink_ip_cnt = 2; //短链接ip池数量
340 | message longlink_ip_info
341 | {
342 | optional string ip = 3; //ip
343 | optional string host = 4; //host
344 | }
345 | repeated longlink_ip_info longlink = 3; //长链接ip池
346 | message shortlink_ip_info
347 | {
348 | optional string ip = 3; //ip
349 | optional string host = 4; //host
350 | }
351 | repeated shortlink_ip_info shortlink = 4; //短链接ip池
352 | }
353 | optional ip_info ip = 3; //长短链接ip
354 | }
355 | optional dns_info dns = 5; //dns信息
356 | }
357 |
358 | //新设备第一次登录初始化请求
359 | message NewInitRequest
360 | {
361 | optional LoginInfo login = 1;
362 | optional string wxid = 2;
363 | optional bytes sync_key_cur = 3; //首次初始化时sync_key_cur = ''
364 | optional bytes sync_key_max = 4; //首次初始化时sync_key_max = ''
365 | optional string language = 5;
366 | }
367 |
368 | //新设备第一次登录初始化服务器响应
369 | message NewInitResponse
370 | {
371 | optional string tag1 = 1;
372 | optional bytes sync_key_cur = 2; //当前synckey二进制数据
373 | optional bytes sync_key_max = 3; //最新synckey二进制数据(若与sync_key_cur不相同,则continue_flag返回1,表示需要继续初始化)
374 | optional int32 continue_flag = 4; //为1时表示仍要继续调用newinit初始化,直到该标志位返回0停止初始化(联系人或未读消息数据太多,无法一次获取完毕)
375 | optional int32 cntList = 6; //tag7结构体数量
376 | repeated common_msg tag7 = 7; //需要根据消息类型解析
377 | }
378 |
379 | //同步消息
380 | message new_sync_req
381 | {
382 | message continue_flag
383 | {
384 | optional int32 flag = 1;
385 | }
386 | optional continue_flag flag = 1; //unknown,must be 0
387 | optional int32 selector = 2; //unknown,just set 7
388 | optional bytes sync_Key = 3; //同步成功后服务器返回的消息id
389 | optional int32 scene = 4; //unkown,just set 3
390 | optional string device = 5; //'android-22'
391 | optional int32 sync_msg_digest = 6; //unknown,just set 1
392 | }
393 |
394 | //同步消息响应
395 | message new_sync_resp
396 | {
397 | optional int32 tag1 = 1;
398 | message new_msg
399 | {
400 | optional int32 cntList = 1; //tag2结构体数量
401 | repeated common_msg tag2 = 2; //需要根据消息类型解析
402 | }
403 | optional new_msg msg = 2; //未读消息
404 | optional int32 tag3 = 3;
405 | optional bytes sync_key = 4; //服务器返回的sync key
406 | optional int32 tag5 = 5;
407 | optional int32 tag6 = 6;
408 | optional int32 utc = 7;
409 | }
410 |
411 | //发送消息请求
412 | message new_send_msg_req
413 | {
414 | optional int32 cnt = 1; //本次发送的消息数量
415 | message msg_info
416 | {
417 | optional Wxid to = 1; //to wxid
418 | optional bytes content = 2; //消息内容
419 | optional int32 type = 3; //消息类型: 文字消息=>1,名片=>42,
420 | optional int32 utc = 4;
421 | optional int32 client_id = 5; //不同消息的utc与client_id必须至少有1个不相同
422 | optional string at_list = 6; //群聊at功能
423 | }
424 | optional msg_info msg = 2; //这里可以是repeated,允许一次发送多条消息
425 | }
426 |
427 | //发送消息响应
428 | message new_send_msg_resp
429 | {
430 | optional mmStr tag1 = 1;
431 | optional int32 cnt = 2;
432 | message result
433 | {
434 | optional int32 code = 1; //错误码 0=>发送成功,-44=>对方开启了朋友验证(被删好友),-22=>消息已发出,但被对方拒收了(被拉黑)
435 | optional Wxid to = 2; //to wxid
436 | optional int32 type = 7; //消息类型
437 | optional int64 svrid = 8; //消息唯一id
438 | }
439 | optional result res = 3; //发送结果
440 | }
441 |
442 | //分享链接请求(也可以分享app链接)
443 | message new_send_app_msg_req
444 | {
445 | optional LoginInfo login = 1; //登录信息
446 | message appmsg_info
447 | {
448 | optional string from_wxid = 1;
449 | optional string app_wxid = 2;
450 | optional int32 tag3 = 3; //unknown just set 0
451 | optional string to_wxid = 4;
452 | optional int32 type = 5; //app_msg type == 5
453 | optional string content = 6; //appmsg具体信息
454 | optional int32 utc = 7;
455 | optional string client_id = 8; //to_wxid+randomint+T+utc
456 | optional int32 tag10 = 10; //unknown just set 3
457 | optional int32 tag11 = 11; //unknown just set 0
458 | }
459 | optional appmsg_info info = 2; //appmsg消息
460 | optional int32 tag4 = 4; //unknown just set 0
461 | optional int32 tag6 = 6; //unknown just set 0
462 | optional string tag7 = 7; //unknown just set ''
463 | optional string fromScene = 8;
464 | optional int32 tag9 = 9; //unknown just set 0
465 | optional int32 tag10 = 10; //unknown just set 0
466 | }
467 |
468 | //分享链接响应
469 | message new_send_app_msg_resp
470 | {
471 | optional mmStr tag1 = 1; //结果?
472 | optional string from_wxid = 3;
473 | optional string to_wxid = 4;
474 | optional int32 sync_key_id = 5;
475 | optional string client_id = 6;
476 | optional int32 utc = 7;
477 | optional int32 type = 8;
478 | optional int64 svrid = 9;
479 | }
480 |
481 | //好友请求(申请或同意)
482 | message verify_user_req
483 | {
484 | optional LoginInfo login = 1; //登录信息
485 | optional int32 op_code = 2; //操作code
486 | optional int32 tag3 = 3; //todo,just set 1
487 | message user_info
488 | {
489 | optional string wxid = 1;
490 | optional string ticket = 2; //v2_name
491 | optional string anti_ticket = 3; //可不填(由getcontact返回,长108位)
492 | optional int32 tag4 = 4;
493 | optional int32 tag8 = 8;
494 | }
495 | optional user_info user = 4; //好友详细信息
496 | optional string content = 5; //打招呼内容
497 | optional int32 tag6 = 6; //just set 1
498 | optional string scene = 7; //scene
499 | optional mmStr device_info = 8; //登录设备信息
500 | }
501 |
502 | //好友请求响应
503 | message verify_user_resp
504 | {
505 | optional mmRes res = 1; //结果
506 | optional string wxid = 2; //wxid
507 | }
508 |
509 | //收红包请求1:获取timingIdentifier
510 | message receive_wxhb_req
511 | {
512 | optional LoginInfo login = 1; //登录信息
513 | optional int32 cmd = 2;
514 | optional int32 tag3 = 3;
515 | optional mmStr info = 4; //红包信息(nativeUrl、id等)
516 | }
517 |
518 | //收红包请求1响应
519 | message receive_wxhb_resp
520 | {
521 | message TAG1
522 | {
523 | optional int32 tag1 = 1;
524 | message TAG1
525 | {
526 | optional string tag1 = 1;
527 | }
528 | }
529 | optional TAG1 tag1 = 1; //没啥用
530 | optional mmStr hb_info = 2; //红包内容,需要取出timingIdentifier字段
531 | optional int32 cmd = 5;
532 | optional int32 ret_code = 6; //错误码
533 | optional string ret_msg = 7; //错误信息
534 | }
535 |
536 | //收红包请求2:拆红包
537 | message open_wxhb_req
538 | {
539 | optional LoginInfo login = 1; //登录信息
540 | optional int32 cmd = 2;
541 | optional int32 tag3 = 3;
542 | optional mmStr info = 4; //红包信息(必须填写timingIdentifier字段)
543 | }
544 |
545 | //收红包请求2响应
546 | message open_wxhb_resp
547 | {
548 | message TAG1
549 | {
550 | optional int32 tag1 = 1;
551 | message TAG1
552 | {
553 | optional string tag1 = 1;
554 | }
555 | }
556 | optional TAG1 tag1 = 1; //没啥用
557 | optional mmStr res = 2; //红包领取结果
558 | optional int32 cmd = 5;
559 | optional int32 ret_code = 6; //错误码 416:需要实名认证
560 | optional string ret_msg = 7; //错误信息
561 | }
562 |
563 | //获取红包详细信息请求
564 | message qry_detail_wxhb_req
565 | {
566 | optional LoginInfo login = 1; //登录信息
567 | optional int32 cmd = 2;
568 | optional int32 tag3 = 3;
569 | optional mmStr info = 4; //红包信息:需要nativeUrl和sendid
570 | }
571 |
572 | //获取红包详细信息响应
573 | message qry_detail_wxhb_resp
574 | {
575 | message TAG1
576 | {
577 | optional int32 tag1 = 1;
578 | message TAG1
579 | {
580 | optional string tag1 = 1;
581 | }
582 | }
583 | optional TAG1 tag1 = 1; //没啥用
584 | optional mmStr res = 2; //红包领取信息
585 | optional int32 cmd = 5;
586 | optional int32 ret_code = 6; //错误码 416:需要实名认证
587 | optional string ret_msg = 7; //错误信息
588 | }
589 |
590 | //发送emoji请求
591 | message send_emoji_req
592 | {
593 | optional LoginInfo login = 1; //登录信息
594 | optional int32 tag2 = 2;
595 | message emoji_info
596 | {
597 | optional string animation_id = 1; //emoji加密文件名;发送游戏emoji时该参数无效
598 | optional int32 tag2 = 2;
599 | optional int32 tag3 = 3;
600 | message TAG4
601 | {
602 | optional int32 tag1 = 1;
603 | }
604 | optional TAG4 tag4 = 4;
605 | optional int32 tag5 = 5;
606 | optional string to_wxid = 6;
607 | optional string game_ext = 7; //游戏参数:
608 | optional string tag8 = 8;
609 | optional string utc = 9; //ms
610 | optional int32 tag11 = 11;
611 | }
612 | optional emoji_info emoji = 3; //发送表情详细信息
613 | optional int32 tag4 = 4;
614 | }
615 |
616 | //发送emoji响应
617 | message send_emoji_resp
618 | {
619 | message result
620 | {
621 | optional int32 code = 1; //错误码;返回0表示发送成功
622 | optional int32 tag2 = 2; //小游戏随机种子?
623 | optional int32 tag3 = 3; //小游戏随机种子?
624 | optional string file_name = 4;
625 | optional int32 svrid = 5; //服务器唯一id
626 | optional int32 tag6 = 6;
627 | }
628 | optional result res = 3; //发送结果
629 | }
630 |
631 | //收款请求
632 | message transfer_operation_req
633 | {
634 | optional LoginInfo login = 1; //登录信息
635 | optional int32 tag2 = 2;
636 | optional int32 tag3 = 3;
637 | optional mmStr info = 4; //转账信息:需要op_code, invalid_time, trans_id, 转账人wxid和transfer_id, 签名使用TDES加密算法(可不签)
638 | }
639 |
640 | //收款结果
641 | message transfer_operation_resp
642 | {
643 | optional mmStr res = 2; //收款结果详细信息
644 | optional int32 ret_code = 6; //错误码
645 | optional string ret_msg = 7; //错误信息
646 | }
647 |
648 | //查询转账记录请求
649 | message transfer_query_req
650 | {
651 | optional LoginInfo login = 1; //登录信息
652 | optional int32 tag2 = 2;
653 | optional int32 tag3 = 3;
654 | optional mmStr info = 4; //转账信息:需要invalid_time, trans_id和transfer_id, 签名使用TDES加密算法(可不签)
655 | }
656 |
657 | //查询转账记录结果
658 | message transfer_query_resp
659 | {
660 | optional mmStr res = 2; //收款结果详细信息
661 | optional int32 ret_code = 6; //错误码
662 | optional string ret_msg = 7; //错误信息
663 | }
664 |
665 | //获取好友详细信息请求
666 | message get_contact_req
667 | {
668 | optional LoginInfo login = 1; //登录信息
669 | optional int32 tag2 = 2;
670 | optional Wxid wxid = 3; //要查询的 好友/群聊 wxid
671 | optional int32 tag4 = 4;
672 | optional int32 tag6 = 6;
673 | message TAG7
674 | {
675 | optional string tag1 = 1;
676 | }
677 | optional TAG7 tag7 = 7;
678 | optional int32 tag8 = 8;
679 | }
680 |
681 | //获取好友详细信息响应
682 | message get_contact_resp
683 | {
684 | optional contact_info info = 3; //好友详细信息
685 | message ticket_info
686 | {
687 | optional string wxid = 1; //查询好友的wxid
688 | optional string ticket = 2; //v2_ticket
689 | }
690 | optional ticket_info ticket = 5; //对方删除/拉黑我 返回v2_name;正常好友该字段为空
691 | }
692 |
693 | //建群聊请求
694 | message create_chatroom_req
695 | {
696 | optional LoginInfo login = 1; //登录信息
697 | message TAG2
698 | {
699 | optional string tag1 = 1;
700 | }
701 | optional TAG2 tag2 = 2;
702 | optional int32 member_cnt = 3; //群成员数量
703 | message member_info
704 | {
705 | optional Wxid wxid = 1;
706 | }
707 | repeated member_info member = 4; //群成员信息
708 | optional int32 tag5 = 5;
709 | }
710 |
711 | //建群聊响应
712 | message create_chatroom_resp
713 | {
714 | message result
715 | {
716 | optional int32 code = 1; //错误码
717 | message err_msg
718 | {
719 | optional string msg = 1; //错误信息
720 | }
721 | optional err_msg msg = 2;
722 | }
723 | optional result res = 1; //建群结果
724 | optional int32 member_cnt = 5; //群成员数量
725 | message member_info
726 | {
727 | optional Wxid wxid = 1; //wxid
728 | message nick_name_info
729 | {
730 | optional string name = 1; //昵称
731 | }
732 | optional nick_name_info nick_name = 3;
733 | optional string qianming = 15; //签名
734 | }
735 | repeated member_info member = 6;
736 | optional Wxid chatroom_wxid = 7; //新建群聊wxid
737 | message chatroom_avatar
738 | {
739 | optional int32 len = 1; //群头像raw data大小
740 | optional bytes data = 2; //群头像 jpg格式 raw data
741 | }
742 | optional chatroom_avatar avatar = 8; //群头像
743 | }
744 |
745 | //面对面建群请求
746 | message mm_facing_create_chatroom_req
747 | {
748 | optional LoginInfo login = 1; //登录信息
749 | optional int32 op_code = 2; //op_code=0:建群等人(未生成群wxid,只返回群成员信息) op_code=1:进群(返回群wxid)
750 | optional string chatroom_pwd = 3; //面对面建群密码(4位;不能是1234)
751 | optional float lon = 4; //面对面建群坐标经度
752 | optional float lat = 5; //面对面建群坐标纬度
753 | optional int32 tag6 = 6;
754 | optional int32 tag9 = 9;
755 | optional string tag10 = 10;
756 | }
757 |
758 | //面对面建群响应
759 | message mm_facing_create_chatroom_resp
760 | {
761 | message result
762 | {
763 | optional int32 code = 1; //错误码
764 | message err_msg
765 | {
766 | optional string msg = 1; //错误信息
767 | }
768 | optional err_msg msg = 2;
769 | }
770 | optional result res = 1; //面对面建群结果
771 | optional int32 member_cnt = 3; //群成员数量(op_code=0阶段)
772 | message member_info
773 | {
774 | optional string msg = 1; //wxid
775 | optional string nick_name = 3; //nick_name
776 | }
777 | repeated member_info member = 4; //群成员信息(op_code=0阶段)
778 | optional string wxid = 5; //面对面群id(op_code=1阶段)
779 | }
780 |
781 | //群聊拉人请求
782 | message add_chatroom_member_req
783 | {
784 | optional LoginInfo login = 1; //登录信息
785 | optional int32 member_cnt = 2; //本次邀请加入群聊的好友数量
786 | message member_info
787 | {
788 | optional Wxid wxid = 1;
789 | }
790 | repeated member_info member = 3; //本次邀请加入群聊的好友信息
791 | message chatroom_info
792 | {
793 | optional string wxid = 1;
794 | }
795 | optional chatroom_info chatroom_wxid = 4; //群聊wxid
796 | optional int32 tag5 = 5;
797 | }
798 |
799 | //群聊拉人响应
800 | message add_chatroom_member_resp
801 | {
802 | message result
803 | {
804 | optional int32 code = 1; //错误码
805 | message err_msg
806 | {
807 | optional string msg = 1; //错误信息
808 | }
809 | optional err_msg msg = 2;
810 | }
811 | optional result res = 1; //群聊拉人结果
812 | }
813 |
814 | //oplog:cmd_id=48 设置群昵称
815 | message op_set_group_nick_name
816 | {
817 | optional string tag1 = 1;
818 | optional string tag2 = 2;
819 | optional bytes tag3 = 3;
820 | }
821 |
822 | //操作请求
823 | message oplog_req
824 | {
825 | message TAG1
826 | {
827 | optional int32 tag1 = 1; //cnt?
828 | message CMD
829 | {
830 | optional int32 cmd_id = 1; //操作类型
831 | message OPTION
832 | {
833 | optional int32 len = 1;
834 | optional bytes data = 2; //不同cmd_id使用不同结构体填充该字段
835 | }
836 | optional OPTION option = 2;
837 | }
838 | optional CMD cmd = 2;
839 | }
840 | optional TAG1 tag1 = 1;
841 | }
842 |
843 | //操作响应
844 | message oplog_resp
845 | {
846 | message result
847 | {
848 | optional bytes code = 2; //错误码 varint
849 | optional string msg = 3; //错误信息
850 | }
851 | optional result res = 2; //结果
852 | }
853 |
854 | //消息撤回请求
855 | message revoke_msg_req
856 | {
857 | optional LoginInfo login = 1; //登录信息
858 | optional string client_id = 2; //客户端发送的消息id
859 | optional int64 new_client_id = 3; //新的客户端消息id
860 | optional int32 utc = 4;
861 | optional int32 tag5 = 5;
862 | optional string from_wxid = 6;
863 | optional string to_wxid = 7;
864 | optional int32 index_of_request = 8;
865 | optional int64 svrid = 9; //撤回消息的svrid
866 | }
867 |
868 | //消息撤回响应
869 | message revoke_msg_resp
870 | {
871 | optional mmRes res = 1; //错误码
872 | optional string response_sys_wording = 3; //消息撤回提示(已撤回或撤回失败原因)
873 | }
874 |
875 | //发布群公告请求
876 | message set_chatroom_announcement_req
877 | {
878 | optional LoginInfo login = 1; //登录信息
879 | optional string chatroom_wxid = 2; //群聊wxid
880 | optional string content = 3; //群公告内容
881 | }
882 |
883 | //发布群公告响应
884 | message set_chatroom_announcement_resp
885 | {
886 | optional mmRes res = 1; //发布群公告结果
887 | }
888 |
889 |
--------------------------------------------------------------------------------
/microchat/plugin/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LSilent/PyMicroChat/e2862b3766f52b924d13f7a75000271dfe1e3d76/microchat/plugin/__init__.py
--------------------------------------------------------------------------------
/microchat/plugin/browser.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import threading
3 | import time
4 | from PyQt5.QtCore import *
5 | from PyQt5.QtWidgets import *
6 | from PyQt5.QtGui import *
7 | from PyQt5.QtWebEngineWidgets import *
8 |
9 | # 浏览器主窗口
10 | class MainWindow(QMainWindow):
11 | def __init__(self, *args, **kwargs):
12 | super().__init__(*args, **kwargs)
13 | self.setWindowTitle("mm security_center")
14 | self.resize(600, 400)
15 | self.show()
16 | self.browser = QWebEngineView()
17 | self.url = sys.argv[1] #浏览器url通过命令行第2个参数传递
18 | self.browser.load(QUrl(self.url))
19 | self.setCentralWidget(self.browser)
20 |
21 | # 通过监测浏览器地址判断登录授权结果
22 | self.timer = threading.Timer(0.1, self.check_url)
23 | self.start = True # 开始监测url
24 | self.timer.start() # 启动定时器
25 | return
26 |
27 | # 监测url,判断网页授权是否结束
28 | def check_url(self):
29 | if self.start:
30 | if self.url == self.browser.url().toString(): # 授权页面url未改变
31 | pass # 继续等待用户操作
32 | else:
33 | if self.browser.url().toString().find('t=login_verify_entrances/w_tcaptcha_ret') > 0: # 滑块验证通过,关闭浏览器
34 | self.start = False
35 | self.close()
36 | return
37 | elif self.browser.url().toString().find('login_verify_entrances/result') > 0: # 授权结果
38 | time.sleep(2) # 显示结果2秒后关闭浏览器
39 | self.close()
40 | return
41 | else: # 继续等待用户操作
42 | pass
43 | self.timer = threading.Timer(0.1, self.check_url)
44 | self.timer.start() # 重启定时器
45 | else:
46 | #点X关闭了浏览器
47 | self.close()
48 |
49 |
50 | if __name__=='__main__':
51 | app = QApplication(sys.argv)
52 | window = MainWindow()
53 | window.show()
54 | app.exec_()
55 | window.start = False
56 | window.close()
57 | sys.exit()
--------------------------------------------------------------------------------
/microchat/plugin/check_friend.py:
--------------------------------------------------------------------------------
1 | import threading
2 | from .. import interface
3 | from .. import mm_pb2
4 | from .. import Util
5 | from . import plugin
6 | from .logger_wrapper import logger
7 |
8 | # 单向好友列表
9 | check_friend_list = {}
10 |
11 | # 测试群
12 | test_chatroom_wxid = ''
13 |
14 | # 打印检测结果
15 | def show():
16 | if check_friend_list:
17 | logger.info('[单向好友检测结果]扎心了,老铁!', 13)
18 | for i in check_friend_list:
19 | logger.info('wxid:{} 昵称:{} 类型:{}'.format(i, check_friend_list[i][0], check_friend_list[i][1]), 11)
20 | else:
21 | logger.info('[单向好友检测结果]恭喜,暂未检测到单向好友!', 11)
22 | return
23 |
24 | # 单向好友检测(扎心,慎用!!!)
25 | # 删除我后对方开启好友验证或拉黑我,拉人入群对方无任何消息通知
26 | # 删除我后对方关闭好友验证(双方仍可互发消息),拉人入群会向对方发送群聊邀请!
27 | # TODO:寻找其他静默检测单向好友方法
28 | def check(chatroom_wxid = '', test_add_chatroom_member = False):
29 | global check_friend_list, test_chatroom_wxid
30 | check_friend_list = {}
31 | # 设置测试群(不设置默认新建面对面群测试)
32 | if chatroom_wxid:
33 | test_chatroom_wxid = chatroom_wxid
34 | # 获取好友列表
35 | friend_list = interface.get_contact_list()
36 | for friend in friend_list:
37 | # 获取好友信息,返回ticket表示被对方拉黑或删除
38 | __, ticket = interface.get_contact(friend[0])
39 | if ticket.ticket:
40 | logger.info('wxid:{} v2数据:{}'.format(ticket.wxid, ticket.ticket))
41 | # 把加入单向好友列表
42 | if friend[0] not in check_friend_list.keys():
43 | # 备注名存在使用备注名;否则使用好友昵称
44 | check_friend_list[friend[0]] = [friend[2], '删好友'] if friend[2] else [friend[1], '删好友']
45 | if test_add_chatroom_member:
46 | if not test_chatroom_wxid:
47 | # 面对面建群
48 | test_chatroom_wxid = interface.mm_facing_create_chatroom()
49 | if test_chatroom_wxid:
50 | # 拉单向好友入群(慎用)
51 | interface.add_chatroom_member(test_chatroom_wxid, [friend[0]])
52 | pass
53 | # 3秒后打印检测结果
54 | threading.Timer(3, show).start()
55 | return
56 |
57 | # 单向好友类型判断(拉黑或删好友)
58 | def check_type(msg):
59 | global check_friend_list
60 | try:
61 | # 过滤拉好友入群失败消息
62 | if test_chatroom_wxid == msg.from_id.id and 10000 == msg.type and Util.wxid == msg.to_id.id:
63 | if msg.raw.content.endswith('拒绝加入群聊'):
64 | # 取昵称
65 | nick_name = msg.raw.content[:msg.raw.content.rfind('拒绝加入群聊')]
66 | for i in check_friend_list:
67 | if nick_name == check_friend_list[i][0]:
68 | check_friend_list[i][1] = '拉黑'
69 | break
70 | except:
71 | pass
72 | return
--------------------------------------------------------------------------------
/microchat/plugin/color_console.py:
--------------------------------------------------------------------------------
1 | '''
2 | 输出彩色终端字体,可扩展,可组合,跨平台...
3 | @example:
4 | print(ColorConsole.red('I am red!'))
5 | -----------------colorama模块的一些常量---------------------------
6 | Fore: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET.
7 | Back: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET.
8 | Style: DIM, NORMAL, BRIGHT, RESET_ALL
9 | '''
10 |
11 | from colorama import init, Fore, Back, Style
12 |
13 | init(autoreset=True)
14 |
15 | RAINBOW = [
16 | Fore.RED, Fore.GREEN, Fore.YELLOW, Fore.BLUE, Fore.MAGENTA, Fore.CYAN,
17 | Fore.WHITE
18 | ]
19 |
20 |
21 | class ColorConsole(object):
22 | @staticmethod
23 | def red(s):
24 | """前景色:红色 背景色:默认"""
25 | return Fore.RED + s
26 |
27 | @staticmethod
28 | def green(s):
29 | """前景色:绿色 背景色:默认"""
30 | return Fore.GREEN + s
31 |
32 | @staticmethod
33 | def yellow(s):
34 | """前景色:黄色 背景色:默认"""
35 | return Fore.YELLOW + s
36 |
37 | @staticmethod
38 | def blue(s):
39 | """前景色:蓝色 背景色:默认"""
40 | return Fore.BLUE + s
41 |
42 | @staticmethod
43 | def magenta(s):
44 | """前景色:洋红色 背景色:默认"""
45 | return Fore.MAGENTA + s
46 |
47 | @staticmethod
48 | def cyan(s):
49 | """前景色:青色 背景色:默认"""
50 | return Fore.CYAN + s
51 |
52 | @staticmethod
53 | def white(s):
54 | """前景色:白色 背景色:默认"""
55 | return Fore.WHITE + s
56 |
57 | @staticmethod
58 | def black(s):
59 | """前景色:黑色 背景色:默认"""
60 | return Fore.BLACK + s
61 |
62 | @staticmethod
63 | def white_green(s):
64 | """前景色:白色 背景色:绿色"""
65 | return Fore.WHITE + Back.GREEN + s
66 |
67 | @staticmethod
68 | def rainbow(s):
69 | """前景色:五彩缤纷 背景色:默认"""
70 | ret = ''
71 | for i, ch in enumerate(s):
72 | ret += RAINBOW[i%7] + ch
73 | return ret
74 |
--------------------------------------------------------------------------------
/microchat/plugin/get_win_browser.py:
--------------------------------------------------------------------------------
1 | import winreg
2 |
3 | BROWSER_PATH = {
4 | '360jisu': 'SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\\360chrome.exe',
5 | 'chrome': 'SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe',
6 | }
7 |
8 | def get_browser_path():
9 | paths_dict = {}
10 | for name in BROWSER_PATH:
11 | handler = winreg.OpenKey(
12 | winreg.HKEY_LOCAL_MACHINE, BROWSER_PATH[name], access=winreg.KEY_READ)
13 | abs_path = winreg.QueryValue(handler, None)
14 | # print(abs_path)
15 | paths_dict[name] = abs_path
16 | return paths_dict
17 |
--------------------------------------------------------------------------------
/microchat/plugin/handle_appmsg.py:
--------------------------------------------------------------------------------
1 | import json
2 | from .. import define
3 | from .. import interface
4 | from .. import mm_pb2
5 | from .. import Util
6 | from . import plugin
7 | from bs4 import BeautifulSoup
8 | from .logger_wrapper import logger
9 |
10 |
11 | # appmsg 消息处理
12 | def appmsg_handler(msg):
13 | # 读取消息类型
14 | try:
15 | soup = BeautifulSoup(msg.raw.content,'html.parser')
16 | msg_type = soup.appmsg.type.contents[0] # 红包: 转账: 2000
17 | except:
18 | pass
19 |
20 | if '2001' == msg_type: # 红包消息
21 | if plugin.TEST_STATE[4]: # 自动抢红包功能开关
22 | auto_recive_hb(msg) # 自动抢红包
23 | qry_detail_wxhb(msg) # 获取红包领取信息
24 | elif '2000' == msg_type: # 转账信息
25 | if plugin.TEST_STATE[4]: # 自动收款功能开关
26 | auto_confirm_transfer(msg) # 自动确认收款
27 | transfer_query(msg) # 查看转账记录
28 | return
29 |
30 |
31 | # 自动抢红包
32 | def auto_recive_hb(msg):
33 | try:
34 | # 解析nativeUrl,获取msgType,channelId,sendId
35 | soup = BeautifulSoup(msg.raw.content,'html.parser')
36 | nativeUrl = soup.msg.appmsg.wcpayinfo.nativeurl.contents[0]
37 | msgType = Util.find_str(nativeUrl,'msgtype=','&')
38 | channelId = Util.find_str(nativeUrl,'&channelid=','&')
39 | sendId = Util.find_str(nativeUrl,'&sendid=','&')
40 |
41 | # 领红包
42 | (ret_code,info) = interface.receive_and_open_wxhb(channelId,msgType,nativeUrl,sendId)
43 | if not ret_code:
44 | logger.info('自动抢红包成功!', 11)
45 | logger.debug('红包详细信息:' + info)
46 | else:
47 | logger.info('红包详细信息:' + info, 13)
48 | except:
49 | logger.info('自动抢红包失败!', 13)
50 | return
51 |
52 | # 查看红包信息
53 | def qry_detail_wxhb(msg):
54 | try:
55 | # 解析nativeUrl,获取sendId
56 | soup = BeautifulSoup(msg.raw.content,'html.parser')
57 | nativeUrl = soup.msg.appmsg.wcpayinfo.nativeurl.contents[0]
58 | sendId = Util.find_str(nativeUrl, '&sendid=', '&')
59 |
60 | # 查看红包领取情况
61 | (ret_code,info) = interface.qry_detail_wxhb(nativeUrl, sendId)
62 | logger.info('查询红包详细信息:\n错误码:{}\n领取信息:{}'.format(ret_code, info), 13)
63 | except:
64 | logger.info('查看红包详细信息失败!', 13)
65 | return
66 |
67 | # 自动确认收款
68 | def auto_confirm_transfer(msg):
69 | # 过滤自己发出去的收款信息(收款成功通知)
70 | if msg.from_id.id == Util.wxid:
71 | return
72 |
73 | try:
74 | # 解析transcationid,transferid,invalidtime
75 | soup = BeautifulSoup(msg.raw.content,'html.parser')
76 | transaction_id = soup.msg.appmsg.wcpayinfo.transcationid.contents[0]
77 | trans_id = soup.msg.appmsg.wcpayinfo.transferid.contents[0]
78 | invalid_time = soup.msg.appmsg.wcpayinfo.invalidtime.contents[0]
79 |
80 | # 确认收款
81 | (ret_code,info) = interface.transfer_operation(invalid_time, trans_id, transaction_id, msg.from_id.id)
82 | if not ret_code:
83 | logger.info('收款成功!', 11)
84 | logger.debug('转账详细信息:' + info)
85 | else:
86 | logger.info('转账详细信息:' + info, 13)
87 | except:
88 | logger.info('自动收款失败!', 13)
89 | return
90 |
91 | # 查询转账记录
92 | def transfer_query(msg):
93 | # 过滤自己发出去的收款信息(收款成功通知)
94 | if msg.from_id.id == Util.wxid:
95 | return
96 |
97 | try:
98 | # 解析transcationid,transferid,invalidtime
99 | soup = BeautifulSoup(msg.raw.content,'html.parser')
100 | trans_id = soup.msg.appmsg.wcpayinfo.transcationid.contents[0]
101 | transfer_id = soup.msg.appmsg.wcpayinfo.transferid.contents[0]
102 | invalid_time = soup.msg.appmsg.wcpayinfo.invalidtime.contents[0]
103 |
104 | # 查询转账记录
105 | (ret_code,info) = interface.transfer_query(invalid_time, trans_id, transfer_id)
106 | logger.info('[查询转账记录]:\n错误码:{}\n转账信息:{}'.format(ret_code, info), 13)
107 | except:
108 | logger.info('查询转账记录失败!', 13)
109 | return
--------------------------------------------------------------------------------
/microchat/plugin/logger_wrapper.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import sys
3 | import os
4 | import time
5 | import ctypes, sys
6 | import threading
7 | import enum
8 | from .color_console import ColorConsole
9 |
10 | __all__ = ['logger']
11 |
12 | STD_INPUT_HANDLE = -10
13 | STD_OUTPUT_HANDLE = -11
14 | STD_ERROR_HANDLE = -12
15 |
16 |
17 | class ColorDefine(enum.IntEnum):
18 |
19 | # 字体颜色定义 ,关键在于颜色编码,由2位十六进制组成,分别取0~f,前一位指的是背景色,后一位指的是字体色
20 | #由于该函数的限制,应该是只有这16种,可以前景色与背景色组合。也可以几种颜色通过或运算组合,组合后还是在这16种颜色中
21 |
22 | # Windows CMD命令行 字体颜色定义 text colors
23 | FOREGROUND_BLACK = 0x00 # black.
24 | FOREGROUND_DARKBLUE = 0x01 # dark blue.
25 | FOREGROUND_DARKGREEN = 0x02 # dark green.
26 | FOREGROUND_DARKSKYBLUE = 0x03 # dark skyblue.
27 | FOREGROUND_DARKRED = 0x04 # dark red.
28 | FOREGROUND_DARKPINK = 0x05 # dark pink.
29 | FOREGROUND_DARKYELLOW = 0x06 # dark yellow.
30 | FOREGROUND_DARKWHITE = 0x07 # dark white.
31 | FOREGROUND_DARKGRAY = 0x08 # dark gray.
32 | FOREGROUND_BLUE = 0x09 # blue.
33 | FOREGROUND_GREEN = 0x0a # green.
34 | FOREGROUND_SKYBLUE = 0x0b # skyblue.
35 | FOREGROUND_RED = 0x0c # red.
36 | FOREGROUND_PINK = 0x0d # pink.
37 | FOREGROUND_YELLOW = 0x0e # yellow.
38 | FOREGROUND_WHITE = 0x0f # white.
39 | FOREGROUND_PURPLE = FOREGROUND_RED | FOREGROUND_BLUE
40 |
41 | # Windows CMD命令行 背景颜色定义 background colors
42 | BACKGROUND_DARKBLUE = 0x10 # dark blue.
43 | BACKGROUND_DARKGREEN = 0x20 # dark green.
44 | BACKGROUND_DARKSKYBLUE = 0x30 # dark skyblue.
45 | BACKGROUND_DARKRED = 0x40 # dark red.
46 | BACKGROUND_DARKPINK = 0x50 # dark pink.
47 | BACKGROUND_DARKYELLOW = 0x60 # dark yellow.
48 | BACKGROUND_DARKWHITE = 0x70 # dark white.
49 | BACKGROUND_DARKGRAY = 0x80 # dark gray.
50 | BACKGROUND_BLUE = 0x90 # blue.
51 | BACKGROUND_GREEN = 0xa0 # green.
52 | BACKGROUND_SKYBLUE = 0xb0 # skyblue.
53 | BACKGROUND_RED = 0xc0 # red.
54 | BACKGROUND_PINK = 0xd0 # pink.
55 | BACKGROUND_YELLOW = 0xe0 # yellow.
56 | BACKGROUND_WHITE = 0xf0 # white.
57 |
58 |
59 | std_out_handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
60 |
61 |
62 | def set_cmd_text_color(color, handle=std_out_handle):
63 | Bool = ctypes.windll.kernel32.SetConsoleTextAttribute(handle, color)
64 | return Bool
65 |
66 |
67 | def reset_color():
68 | # set_cmd_text_color(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE) #reset white
69 | set_cmd_text_color(ColorDefine.FOREGROUND_GREEN) #reset green
70 |
71 |
72 | def __singletion(cls):
73 | """
74 | 单例模式的装饰器函数
75 | :param cls: 实体类
76 | :return: 返回实体类对象
77 | """
78 | instances = {}
79 |
80 | def getInstance(*args, **kwargs):
81 | if cls not in instances:
82 | instances[cls] = cls(*args, **kwargs)
83 | return instances[cls]
84 |
85 | return getInstance
86 |
87 |
88 | # @__singletion
89 | class LoggerWrapper(object):
90 | def __init__(self):
91 | """
92 | 单例初始化
93 | :param out: 设置输出端:0:默认控制台
94 | :return: 返回日志对象
95 | """
96 | self.out = 0
97 | if os.path.exists(os.getcwd() + '/log') is False:
98 | os.mkdir(os.getcwd() + '/log')
99 |
100 | def config(self, appName, logFileName=None, level=logging.INFO, out=2, fore_color = ColorDefine.FOREGROUND_GREEN):
101 | """
102 | 获取日志处理对象
103 | :param appName: 应用程序名
104 | :param logFileName: 日志文件名
105 | :param out: 设置输出端:0:默认控制台,1:输入文件,其他:logger指定的控制台和文件都输出
106 | : 2: 定制的控制台输出和文件输出
107 | :return: 返回日志对象
108 | """
109 | self.appName = appName
110 | if logFileName is None:
111 | self.logFileName = os.getcwd() + '/log/' + time.strftime(
112 | "%Y-%m-%d", time.localtime()) + ".log"
113 | self.log_level = level
114 | self.out = out
115 | self.fore_color = fore_color
116 | self.logger_file, self.logger_console = self.getLogger()
117 |
118 | def getLogger(self):
119 | # 获取logging实例
120 | logger_file = logging.getLogger(self.appName)
121 | logger_console = logging.getLogger('streamer')
122 | # 指定输出的格式
123 | formatter = logging.Formatter(
124 | '%(name)s %(asctime)s %(levelname)8s: %(message)s')
125 |
126 | # 文件日志
127 | file_handler = logging.FileHandler(self.logFileName, encoding='utf-8')
128 | file_handler.setFormatter(formatter)
129 |
130 | # 控制台日志
131 | console_handler = logging.StreamHandler(sys.stdout)
132 | console_handler.setFormatter(formatter)
133 |
134 | # # 指定日志的最低输出级别
135 | logger_file.setLevel(self.log_level) # 20 INFO
136 | logger_console.setLevel(self.log_level) # 20
137 | # 为logger添加具体的日志处理器输出端
138 | if self.out == 1:
139 | logger_file.addHandler(file_handler)
140 | elif self.out == 0:
141 | logger_console.addHandler(console_handler)
142 | else:
143 | logger_file.addHandler(file_handler)
144 | logger_console.addHandler(console_handler)
145 |
146 | logger_file.propagate = False
147 | logger_console.propagate = False
148 |
149 | return logger_file, logger_console
150 |
151 | def setLevel(self, level):
152 | if self.out == 1:
153 | self.log_level = self.logger_file.setLevel(level)
154 | elif self.out == 0:
155 | self.log_level = self.logger_console.setLevel(level)
156 | else:
157 | self.log_level = self.logger_file.setLevel(level)
158 | self.log_level = self.logger_console.setLevel(level)
159 |
160 | def debug(self, msg, color=None, *args, **kwargs):
161 | try:
162 | msg1 = msg.encode('gbk', 'ignore').decode('gbk', 'ignore')
163 | except:
164 | msg1 = ''
165 |
166 | if color is None:
167 | set_cmd_text_color(ColorDefine.FOREGROUND_WHITE)
168 | else:
169 | set_cmd_text_color(color)
170 |
171 | if self.out == 1:
172 | self.logger_file.debug(msg, *args, **kwargs)
173 | elif self.out == 0:
174 | self.logger_console.debug(msg1, *args, **kwargs)
175 | else:
176 | self.logger_file.debug(msg, *args, **kwargs)
177 | self.logger_console.debug(msg1, *args, **kwargs)
178 | set_cmd_text_color(self.fore_color)
179 |
180 | def info(self, msg, color=None, *args, **kwargs):
181 | try:
182 | msg1 = msg.encode('gbk', 'ignore').decode('gbk', 'ignore')
183 | except:
184 | msg1 = ''
185 |
186 | if color is None:
187 | set_cmd_text_color(self.fore_color)
188 | else:
189 | set_cmd_text_color(color)
190 |
191 | if self.out == 1:
192 | self.logger_file.info(msg, *args, **kwargs)
193 | elif self.out == 0:
194 | self.logger_console.info(msg1, *args, **kwargs)
195 | else:
196 | self.logger_file.info(msg, *args, **kwargs)
197 | self.logger_console.info(msg1, *args, **kwargs)
198 | set_cmd_text_color(self.fore_color)
199 |
200 | def warning(self, msg, color=None, *args, **kwargs):
201 | try:
202 | msg1 = msg.encode('gbk', 'ignore').decode('gbk', 'ignore')
203 | except:
204 | msg1 = ''
205 |
206 | if color is None:
207 | set_cmd_text_color(ColorDefine.FOREGROUND_YELLOW)
208 | else:
209 | set_cmd_text_color(color)
210 |
211 | if self.out == 1:
212 | self.logger_file.warning(msg, *args, **kwargs)
213 | elif self.out == 0:
214 | self.logger_console.warning(msg1, *args, **kwargs)
215 | else:
216 | self.logger_file.warning(msg, *args, **kwargs)
217 | self.logger_console.warning(msg1, *args, **kwargs)
218 | set_cmd_text_color(self.fore_color)
219 |
220 | def warn(self, msg, color=None, *args, **kwargs):
221 | try:
222 | msg1 = msg.encode('gbk', 'ignore').decode('gbk', 'ignore')
223 | except:
224 | msg1 = ''
225 |
226 | if color is None:
227 | set_cmd_text_color(ColorDefine.FOREGROUND_DARKYELLOW)
228 | else:
229 | set_cmd_text_color(color)
230 |
231 | if self.out == 1:
232 | self.logger_file.warn(msg, *args, **kwargs)
233 | elif self.out == 0:
234 | self.logger_console.warn(msg1, *args, **kwargs)
235 | else:
236 | self.logger_file.warn(msg, *args, **kwargs)
237 | self.logger_console.warn(msg1, *args, **kwargs)
238 | set_cmd_text_color(self.fore_color)
239 |
240 | def error(self, msg, color=None, *args, **kwargs):
241 | try:
242 | msg1 = msg.encode('gbk', 'ignore').decode('gbk', 'ignore')
243 | except:
244 | msg1 = ''
245 |
246 | if color is None:
247 | set_cmd_text_color(ColorDefine.FOREGROUND_RED)
248 | else:
249 | set_cmd_text_color(color)
250 |
251 | if self.out == 1:
252 | self.logger_file.error(msg, *args, **kwargs)
253 | elif self.out == 0:
254 | self.logger_console.error(msg1, *args, **kwargs)
255 | else:
256 | self.logger_file.error(msg, *args, **kwargs)
257 | self.logger_console.error(msg1, *args, **kwargs)
258 | set_cmd_text_color(self.fore_color)
259 |
260 | def critical(self, msg, color=None, *args, **kwargs):
261 | try:
262 | msg1 = msg.encode('gbk', 'ignore').decode('gbk', 'ignore')
263 | except:
264 | msg1 = ''
265 |
266 | if color is None:
267 | set_cmd_text_color(ColorDefine.FOREGROUND_DARKPINK)
268 | else:
269 | set_cmd_text_color(color)
270 |
271 | if self.out == 1:
272 | self.logger_file.critical(msg, *args, **kwargs)
273 | elif self.out == 0:
274 | self.logger_console.critical(msg1, *args, **kwargs)
275 | else:
276 | self.logger_file.critical(msg, *args, **kwargs)
277 | self.logger_console.critical(msg1, *args, **kwargs)
278 | set_cmd_text_color(self.fore_color)
279 |
280 | # 定义一个logger
281 | logger = LoggerWrapper()
282 |
--------------------------------------------------------------------------------
/microchat/plugin/plugin.py:
--------------------------------------------------------------------------------
1 | import json
2 | import random
3 | import time
4 | from .. import define
5 | from .. import interface
6 | from .. import mm_pb2
7 | from .. import Util
8 | from . import verify_friend
9 | from . import handle_appmsg
10 | from . import tuling_robot
11 | from . import check_friend
12 | from . import revoke_joke
13 | from .logger_wrapper import logger
14 |
15 | # 测试命令
16 | state = lambda i: '已开启' if i else '已关闭'
17 | TEST_KEY_WORD = ('测试分享链接', '测试好友列表', '图灵机器人', '自动通过好友申请', '自动抢红包/自动收款', '测试扔骰子', '测试面对面建群', '检测单向好友', '测试消息撤回', '测试拉黑', '测试发布群公告')
18 | # 测试开关
19 | TEST_STATE = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
20 |
21 | # 插件黑名单(不处理该wxid的消息)
22 | plugin_blacklist = ['weixin', ]
23 |
24 | # 测试接口
25 | def test(msg):
26 | global TEST_STATE
27 | # 来自群聊的消息不处理
28 | if '测试' == msg.raw.content: # help
29 | send_text = '当前支持的测试指令:\n'
30 | for i in range(len(TEST_KEY_WORD)):
31 | send_text += '[{}]{}({})\n'.format(i, TEST_KEY_WORD[i],state(TEST_STATE[i]))
32 | interface.new_send_msg(msg.from_id.id, send_text.encode(encoding="utf-8"))
33 | return False
34 | elif TEST_KEY_WORD[0] == msg.raw.content or '0' == msg.raw.content: # 测试分享链接
35 | interface.send_app_msg(msg.from_id.id, '贪玩蓝月', '大渣好,我系咕天乐,我是渣渣辉,贪挽懒月,介系一个你没有挽过的船新版本', 'http://www.gov.cn/','https://ss0.bdstatic.com/-0U0bnSm1A5BphGlnYG/tam-ogel/f1d67c57e00fea1dc0f90210d7add1ad_121_121.jpg')
36 | return False
37 | elif TEST_KEY_WORD[1] == msg.raw.content or '1' == msg.raw.content: # 测试获取好友列表
38 | interface.new_send_msg(msg.from_id.id, Util.str2bytes('我有好友{}人,加入群聊{}个,已关注公众号{}个,黑名单中好友{}位'.format(len(interface.get_contact_list(Util.CONTACT_TYPE_FRIEND)), len(interface.get_contact_list(
39 | Util.CONTACT_TYPE_CHATROOM)), len(interface.get_contact_list(Util.CONTACT_TYPE_OFFICAL)), len(interface.get_contact_list(Util.CONTACT_TYPE_BLACKLIST)))))
40 | return False
41 | elif TEST_KEY_WORD[2] == msg.raw.content or '2' == msg.raw.content: # 图灵机器人开关
42 | TEST_STATE[2] = not TEST_STATE[2]
43 | interface.new_send_msg(msg.from_id.id, '{}({})'.format(TEST_KEY_WORD[2],state(TEST_STATE[2])).encode(encoding="utf-8"))
44 | return False
45 | elif TEST_KEY_WORD[3] == msg.raw.content or '3' == msg.raw.content: # 自动通过好友申请开关
46 | TEST_STATE[3] = not TEST_STATE[3]
47 | interface.new_send_msg(msg.from_id.id, '{}({})'.format(TEST_KEY_WORD[3],state(TEST_STATE[3])).encode(encoding="utf-8"))
48 | return False
49 | elif TEST_KEY_WORD[4] == msg.raw.content or '4' == msg.raw.content: # 自动抢红包/自动收款开关
50 | TEST_STATE[4] = not TEST_STATE[4]
51 | interface.new_send_msg(msg.from_id.id, '{}({})'.format(TEST_KEY_WORD[4],state(TEST_STATE[4])).encode(encoding="utf-8"))
52 | return False
53 | elif TEST_KEY_WORD[5] == msg.raw.content or '5' == msg.raw.content: # 测试发送骰子表情
54 | interface.new_send_msg(msg.from_id.id, '送你一波666'.encode(encoding="utf-8"))
55 | interface.send_emoji(msg.from_id.id,'68f9864ca5c0a5d823ed7184e113a4aa','1','9')
56 | interface.send_emoji(msg.from_id.id,'514914788fc461e7205bf0b6ba496c49','2','9')
57 | interface.send_emoji(msg.from_id.id,'9a21c57defc4974ab5b7c842e3232671','1','9')
58 | return False
59 | elif TEST_KEY_WORD[6] == msg.raw.content or '6' == msg.raw.content: # 测试面对面建群
60 | wxid = interface.mm_facing_create_chatroom('{}'.format(random.randint(2222, 9999)))
61 | if wxid:
62 | interface.add_chatroom_member(wxid, [msg.from_id.id, ])
63 | # 刚建的面对面群立即拉人对方无法收到通知(延迟2秒后再拉人对方才会收到进群通知),这里发消息到群聊at所有人测试对方是否入群
64 | interface.at_all_in_group(wxid, '你们已经在我的群聊里了')
65 | return False
66 | elif TEST_KEY_WORD[7] == msg.raw.content or '7' == msg.raw.content: # 检测单向好友
67 | if TEST_STATE[7]:
68 | interface.new_send_msg(msg.from_id.id, '开始检测单向好友......'.encode(encoding="utf-8"))
69 | check_friend.check()
70 | return False
71 | elif TEST_KEY_WORD[8] == msg.raw.content or '8' == msg.raw.content: # 测试消息撤回
72 | revoke_joke.revoke_joke(msg.from_id.id, '对方', '并亲了你一口')
73 | return False
74 | elif TEST_KEY_WORD[9] == msg.raw.content or '9' == msg.raw.content: # 测试黑名单
75 | interface.ban_friend(msg.from_id.id, True)
76 | interface.new_send_msg(msg.from_id.id, '你被我拉黑了,5秒后恢复好友关系'.encode())
77 | time.sleep(5)
78 | interface.ban_friend(msg.from_id.id, False)
79 | interface.new_send_msg(msg.from_id.id, '已从黑名单中移除'.encode())
80 | return False
81 | elif TEST_KEY_WORD[10] == msg.raw.content or '10' == msg.raw.content: # 测试群公告
82 | # 面对面建群
83 | wxid = interface.mm_facing_create_chatroom()
84 | if wxid:
85 | # 拉人入群
86 | interface.add_chatroom_member(wxid, [msg.from_id.id])
87 | # 设置群公告
88 | interface.set_chatroom_announcement(wxid, '天王盖地虎')
89 | # 设置群聊名
90 | interface.set_friend_name(wxid, '宝塔镇河妖')
91 | return False
92 | return True
93 |
94 | # 处理消息
95 | def dispatch(msg):
96 | # 过滤wxid
97 | if msg.from_id.id in plugin_blacklist:
98 | return
99 |
100 | # 文字消息
101 | if 1 == msg.type:
102 | # 测试接口
103 | if test(msg):
104 | if TEST_STATE[2]:
105 | # 机器人回复消息
106 | tuling_robot.tuling_robot(msg)
107 | # 好友请求消息
108 | elif 37 == msg.type:
109 | if TEST_STATE[3]:
110 | # 自动通过好友申请
111 | verify_friend.auto_verify_friend(msg)
112 | # appmsg
113 | elif 49 == msg.type:
114 | handle_appmsg.appmsg_handler(msg)
115 | # 系统消息
116 | elif 10000 == msg.type:
117 | if TEST_STATE[7]:
118 | # 单向好友检测
119 | check_friend.check_type(msg)
120 | else:
121 | pass
122 | return
123 |
--------------------------------------------------------------------------------
/microchat/plugin/revoke_joke.py:
--------------------------------------------------------------------------------
1 | from .. import define
2 | from .. import interface
3 | from .. import mm_pb2
4 | from .. import Util
5 | from . import plugin
6 |
7 |
8 | # 群聊中: XXX撤回了一条消息,并XXX
9 | def revoke_joke(wxid, nick_name, text):
10 | # 面对面建群
11 | chatroom_wxid = interface.mm_facing_create_chatroom()
12 |
13 | if chatroom_wxid:
14 | # 把对方拉进群
15 | interface.add_chatroom_member(chatroom_wxid, [wxid, ])
16 |
17 | # 拼接群昵称字串
18 | real_nick_name = b'\xef\xbb\xa9' + text.encode() + b'\xef\xbb\xa9' + nick_name.encode()
19 |
20 | # 设置群昵称
21 | interface.set_group_nick_name(chatroom_wxid, real_nick_name)
22 |
23 | # 发送任意消息
24 | ret_code, svrid = interface.new_send_msg(chatroom_wxid, ' '.encode())
25 |
26 | # 撤回刚刚发送的消息
27 | interface.revoke_msg(chatroom_wxid, svrid)
28 |
29 | # 恢复群昵称
30 | interface.set_group_nick_name(chatroom_wxid, b'')
31 |
32 | return
--------------------------------------------------------------------------------
/microchat/plugin/tuling_robot.py:
--------------------------------------------------------------------------------
1 | import json
2 | from .. import define
3 | from .. import interface
4 | from .. import mm_pb2
5 | from .. import Util
6 | from bs4 import BeautifulSoup
7 | from .logger_wrapper import logger
8 |
9 | # 图灵机器人接口
10 | TULING_HOST = 'openapi.tuling123.com'
11 | TULING_API = 'http://openapi.tuling123.com/openapi/api/v2'
12 | # 图灵机器人key
13 | TULING_KEY = '460a124248234351b2095b57b88cffd2'
14 |
15 | # 图灵机器人
16 | def tuling_robot(msg):
17 | # 本条消息是否回复
18 | need_reply = False
19 | # 消息内容预处理
20 | send_to_tuling_content = msg.raw.content
21 | reply_prefix = ''
22 | reply_at_wxid = ''
23 | # 群聊消息:只回复@自己的消息/消息内容过滤掉sender_wxid
24 | if msg.from_id.id.endswith('@chatroom'):
25 | # 首先判断本条群聊消息是否at我:
26 | try:
27 | soup = BeautifulSoup(msg.ex_info,'html.parser')
28 | at_user_list = soup.msgsource.atuserlist.contents[0].split(',')
29 | if Util.wxid in at_user_list: # 群聊中有@我的消息
30 | # 群聊消息以'sender_wxid:\n'起始
31 | send_to_tuling_content = msg.raw.content[msg.raw.content.find('\n') + 1:]
32 | # 解析@我的人的昵称
33 | reply_nick_name = Util.find_str(msg.xmlContent, '