├── .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, '