├── README.md ├── core ├── client.py ├── const.py ├── croto │ ├── __init__.py │ ├── ecdh.py │ ├── gid.py │ ├── hash.py │ ├── offical.py │ └── qqtea.py ├── entities │ ├── __init__.py │ ├── event.py │ ├── message.py │ ├── packet.py │ └── struct.py ├── protocol │ ├── event │ │ ├── __init__.py │ │ ├── group_msg.py │ │ ├── msg_receipt.py │ │ └── private_msg.py │ ├── pcapi │ │ ├── __init__.py │ │ └── send_msg.py │ ├── webapi │ │ ├── __init__.py │ │ ├── custom_api.py │ │ └── offical_api.py │ └── wtlogin │ │ ├── __init__.py │ │ ├── p1_0825_ping │ │ ├── __init__.py │ │ ├── pack.py │ │ └── unpack.py │ │ ├── p2_0818_qrcode │ │ ├── __init__.py │ │ ├── pack.py │ │ └── unpack.py │ │ ├── p3_0819_scan │ │ ├── __init__.py │ │ ├── pack.py │ │ └── unpack.py │ │ ├── p4_0836_login │ │ ├── __init__.py │ │ ├── pack.py │ │ └── unpack.py │ │ ├── p5_0828_session │ │ ├── __init__.py │ │ ├── pack.py │ │ └── unpack.py │ │ ├── p6_00ec_online │ │ ├── __init__.py │ │ ├── pack.py │ │ └── unpack.py │ │ ├── p7_001d_skey │ │ ├── __init__.py │ │ ├── pack.py │ │ └── unpack.py │ │ ├── p8_0058_heartbeat │ │ ├── __init__.py │ │ ├── pack.py │ │ └── unpack.py │ │ ├── p9_0062_login_out │ │ ├── __init__.py │ │ └── pack.py │ │ └── wtlogin.py └── utils │ ├── __init__.py │ ├── qrcode.py │ ├── rand.py │ ├── socket.py │ └── stream.py ├── default ├── __init__.py ├── bot.py └── plugin.py ├── example.py └── logger.py /README.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | 因为之前用的轻聊版协议失效, 所以基于最新的 PCQQ9.7.5 协议重新开发 4 | 5 | 需要安装的第三方库有: `cryptography`, `httpx`, `pillow` 6 | 7 | - pip install cryptography==3.4.8 8 | - pip install httpx 9 | - pip install jsondataclass 10 | - ~~pip install pillow~~ (默认调用系统默认应用打开二维码, 如果无法调用则需要 pillow 库在终端中打印) 11 | 12 | 目前完善了一小部分, 可以 clone 本项目并运行 `example.py` 中的代码查看效果 13 | 14 | 估摸着这个垃圾玩意也不会有什么用户, 所以本项目将一直处于佛系更新中 15 | -------------------------------------------------------------------------------- /core/client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import traceback 3 | 4 | from socket import inet_aton 5 | from functools import partial 6 | from asyncio import ( 7 | Queue, 8 | new_event_loop, 9 | run_coroutine_threadsafe 10 | ) 11 | 12 | from core.entities import ( 13 | Packet, 14 | QQStruct, 15 | PacketManger, 16 | ) 17 | 18 | from core import croto, const 19 | 20 | from core.utils import ( 21 | UDPSocket, 22 | rand_udp_host 23 | ) 24 | 25 | 26 | class QQClient: 27 | def __init__(self, uin: int = 0): 28 | self.loop = new_event_loop() 29 | 30 | self.sock = UDPSocket( 31 | host=rand_udp_host(), 32 | port=const.UDP_PORT, 33 | ) 34 | self.manage = PacketManger() 35 | 36 | root = os.path.join(os.getcwd(), "data") 37 | if uin != 0: 38 | root = os.path.join(root, str(uin)) 39 | 40 | if not os.path.exists(root): 41 | os.makedirs(root) 42 | 43 | self.stru = QQStruct( 44 | path=root, 45 | ecdh=croto.ECDH(), 46 | addr=(self.sock.host, self.sock.port), 47 | server_ip=inet_aton(self.sock.host) 48 | ) 49 | 50 | self.run_task = self.loop.run_until_complete 51 | self.add_task = partial(run_coroutine_threadsafe, loop=self.loop) 52 | 53 | def send(self, packet: Packet): 54 | data = packet.encode() 55 | self.sock.send(data) 56 | 57 | def recv(self, tea_key: bytes): 58 | data = self.sock.recv() 59 | return Packet.from_raw(data, tea_key) 60 | 61 | async def recv_and_exec(self, tea_key: bytes): 62 | try: 63 | packet = self.recv(tea_key) 64 | await self.manage.exec_all(packet) 65 | except Exception as err: 66 | print('发生异常: ', traceback.format_exc()) -------------------------------------------------------------------------------- /core/const.py: -------------------------------------------------------------------------------- 1 | __bytes = bytes.fromhex 2 | 3 | Header = "02 3B 41" 4 | Tail = "03" 5 | 6 | StructVersion = __bytes("03 00 00 00 01 01 01 00 00 6A 9C 00 00 00 00") 7 | BodyVersion = __bytes("02 00 00 00 01 01 01 00 00 6A 9C") 8 | FuncVersion = __bytes("04 00 00 00 01 01 01 00 00 6A 9C 00 00 00 00 00 00 00 00") 9 | VMainVer = __bytes("3B 41") 10 | 11 | EcdhVersion = __bytes("01 03") 12 | DWQDVersion = __bytes("04 04 04 00") 13 | SsoVersion = __bytes("00 00 04 61") 14 | ClientVersion = __bytes("00 00 17 41") 15 | 16 | RandKey = __bytes("66 D0 9F 63 A2 37 02 27 13 17 3B 1E 01 1C A9 DA") 17 | ServiceId = __bytes("00 00 00 01") 18 | DeviceID = __bytes("EE D2 37 A4 94 D3 7A 04 7D 98 18 E8 EE DF B0 D6 96 B3 A3 1C BB 4F 95 6A 3E 6C EE F5 02 C5 5A 1F") 19 | 20 | TCP_PORT = 443 21 | UDP_PORT = 8000 22 | HeartBeatInterval = 30.0 23 | 24 | StateOnline = 10 # 上线 25 | StateLeave = 30 # 离开 26 | StateInvisible = 40 # 隐身 27 | StateBusy = 50 # 忙碌 28 | StateCallMe = 60 # Q我吧 29 | StateUndisturb = 70 # 请勿打扰 -------------------------------------------------------------------------------- /core/croto/__init__.py: -------------------------------------------------------------------------------- 1 | from .ecdh import ECDH 2 | 3 | from .gid import gid_from_group 4 | 5 | from .hash import ( 6 | md5, 7 | sha256, 8 | sha512, 9 | sha1024, 10 | rand_str, 11 | rand_str2, 12 | sub_16F90 13 | ) 14 | 15 | from .offical import create_official 16 | 17 | from .qqtea import tea_encrypt, tea_decrypt 18 | -------------------------------------------------------------------------------- /core/croto/ecdh.py: -------------------------------------------------------------------------------- 1 | from cryptography.hazmat.bindings._openssl import ffi, lib 2 | 3 | 4 | class ECDH: 5 | def __init__(self): 6 | self.public_key = bytes(25) 7 | self.share_key = bytes(16) 8 | 9 | self.ec_key = lib.EC_KEY_new_by_curve_name(711) 10 | self.group = lib.EC_KEY_get0_group(self.ec_key) 11 | self.point = lib.EC_POINT_new(self.group) 12 | 13 | if lib.EC_KEY_generate_key(self.ec_key) == 1: 14 | lib.EC_POINT_point2oct(self.group, lib.EC_KEY_get0_public_key( 15 | self.ec_key), 2, self.public_key, len(self.public_key), ffi.NULL) 16 | 17 | buf = bytes([ 18 | 4, 191, 71, 161, 207, 120, 166, 19 | 41, 102, 139, 11, 195, 159, 142, 20 | 84, 201, 204, 243, 182, 56, 75, 21 | 8, 184, 174, 236, 135, 218, 159, 22 | 48, 72, 94, 223, 231, 103, 150, 23 | 157, 193, 163, 175, 17, 21, 254, 24 | 13, 204, 142, 11, 23, 202, 207 25 | ]) 26 | if lib.EC_POINT_oct2point(self.group, self.point, buf, len(buf), ffi.NULL) == 1: 27 | lib.ECDH_compute_key(self.share_key, len( 28 | self.share_key), self.point, self.ec_key, ffi.NULL) 29 | 30 | def twice(self, tk_key: bytes): 31 | twice_key = bytes(16) 32 | 33 | if lib.EC_POINT_oct2point(self.group, self.point, tk_key, len(tk_key), ffi.NULL) == 1: 34 | lib.ECDH_compute_key(twice_key, len(twice_key), 35 | self.point, self.ec_key, ffi.NULL) 36 | 37 | return twice_key 38 | -------------------------------------------------------------------------------- /core/croto/gid.py: -------------------------------------------------------------------------------- 1 | def gid_from_group(group_id: int) -> int: 2 | group = str(group_id) 3 | left = int(group[0:-6]) 4 | 5 | if left >= 0 and left <= 10: 6 | right = group[-6:] 7 | gid = str(left + 202) + right 8 | elif left >= 11 and left <= 19: 9 | right = group[-6:] 10 | gid = str(left + 469) + right 11 | elif left >= 20 and left <= 66: 12 | left = int(str(left)[0:1]) 13 | right = group[-7:] 14 | gid = str(left + 208) + right 15 | elif left >= 67 and left <= 156: 16 | right = group[-6:] 17 | gid = str(left + 1943) + right 18 | elif left >= 157 and left <= 209: 19 | left = int(str(left)[0:2]) 20 | right = group[-7:] 21 | gid = str(left + 199) + right 22 | elif left >= 210 and left <= 309: 23 | left = int(str(left)[0:2]) 24 | right = group[-7:] 25 | gid = str(left + 389) + right 26 | elif left >= 310 and left <= 335: 27 | left = int(str(left)[0:2]) 28 | right = group[-7:] 29 | gid = str(left + 349) + right 30 | elif left >= 336 and left <= 386: 31 | left = int(str(left)[0:3]) 32 | right = group[-6:] 33 | gid = str(left + 2265) + right 34 | elif left >= 387 and left <= 499: 35 | left = int(str(left)[0:3]) 36 | right = group[-6:] 37 | gid = str(left + 3490) + right 38 | elif left >= 500: 39 | return int(group) 40 | return int(gid) 41 | -------------------------------------------------------------------------------- /core/croto/hash.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import random 3 | import hashlib 4 | 5 | 6 | def memwrite(dest: bytearray, i: int, value: bytes): 7 | for i2 in range(len(value)): 8 | dest[i + i2] = value[i2] 9 | 10 | 11 | def memset(dest: bytearray, value: int): 12 | for i in range(len(dest)): 13 | dest[i] = value 14 | 15 | # ------------------------------------------ 16 | 17 | 18 | def md5(src: bytes): 19 | return hashlib.md5(src).digest() 20 | 21 | 22 | def sha256(data: bytes, val: bytes): 23 | v17 = bytearray(8) 24 | 25 | memwrite(v17, 0, (953751936).to_bytes(4, "little")) 26 | memwrite(v17, 4, (27660).to_bytes(2, "little")) 27 | memwrite(v17, 6, (28329).to_bytes(2, "little")) 28 | 29 | v7 = bytearray(hashlib.sha1(data).digest()) 30 | v9 = len(v7) 31 | 32 | v8 = 0 33 | while v7 != bytearray(0): 34 | b = v7[v8] ^ (val[v8 % 20] + v17[v8 % 7]) 35 | v7[v8] = ctypes.c_ubyte(b).value 36 | 37 | v8 += 1 38 | if v8 >= v9: 39 | break 40 | 41 | return bytes(v7) 42 | 43 | 44 | def sha512(data: bytes, val: bytes): 45 | v7 = bytearray(64) 46 | v11 = bytearray(8) 47 | v15 = bytearray(64) 48 | v19 = bytearray(64) 49 | 50 | memwrite(v11, 0, (1980729049).to_bytes(4, "little")) 51 | memwrite(v11, 4, (22840).to_bytes(2, "little")) 52 | memwrite(v11, 6, (108).to_bytes(1, "little")) 53 | 54 | memwrite(v7, 0, val) 55 | memset(v19, 22) 56 | memset(v15, 60) 57 | 58 | for i in range(64): 59 | v19[i] = v19[i] ^ v7[i] 60 | v15[i] = v15[i] ^ v7[i] 61 | 62 | v18 = hashlib.sha256(bytes(v19) + data).digest() 63 | v20 = bytearray(hashlib.sha256(v15 + v18[:36]).digest()) 64 | 65 | v9 = 0 66 | while v20 != bytearray(0): 67 | b = v20[v9] ^ v11[v9 % 7] 68 | v20[v9] = ctypes.c_ubyte(b).value 69 | v9 += 1 70 | 71 | if v9 >= 32: 72 | break 73 | 74 | return bytes(v20) 75 | 76 | 77 | def sha1024(data: bytes, val: bytes): 78 | v22 = bytearray(7) 79 | v25 = bytearray(5) 80 | 81 | memwrite(v22, 0, (249112998).to_bytes(4, "little")) 82 | memwrite(v22, 4, (39555).to_bytes(2, "little")) 83 | memwrite(v22, 6, (194).to_bytes(1, "little")) 84 | 85 | memwrite(v25, 0, (2840686918).to_bytes(4, "little")) 86 | memwrite(v25, 4, (33).to_bytes(1, "little")) 87 | 88 | v16 = bytearray(hashlib.sha1(data).digest()) 89 | v9 = len(v16) 90 | v8 = 0 91 | 92 | while v16 != bytearray(0): 93 | b = v16[v8] ^ (v8 + val[v8 % 20] + v25[v8 % 5] + v22[v8 % 7]) 94 | v16[v8] = ctypes.c_ubyte(b).value 95 | 96 | v8 += 1 97 | if v8 >= v9: 98 | break 99 | 100 | return bytes(v16) 101 | 102 | 103 | def rand_str(v1_3: bytearray): 104 | v1 = bytearray(10) 105 | memset(v1, 11) 106 | 107 | v3 = bytearray(20) 108 | memset(v3, 12) 109 | memwrite(v3, 0, (16204).to_bytes(2, "little")) 110 | memwrite(v3, 2, (928279336).to_bytes(4, "little")) 111 | memwrite(v3, 6, (610353194).to_bytes(4, "little")) 112 | 113 | v16 = bytearray(10) 114 | memwrite(v16, 0, (24896).to_bytes(2, "little")) 115 | memwrite(v16, 2, (910043961).to_bytes(4, "little")) 116 | memwrite(v16, 6, (1327901016).to_bytes(4, "little")) 117 | 118 | v11 = bytes([97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 111, 112, 113, 114, 115, 116, 117, 119 | 118, 119, 120, 121, 122, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 69, 70, 71, 77, 78]) 120 | v2 = 3 121 | while True: 122 | v1[v2] = v11[random.randint(0, 32767) % 44] 123 | v2 += 1 124 | if v2 > 9: 125 | break 126 | v1[0] = v1[3] 127 | v1[1] = v1[6] 128 | v1[2] = v1[9] 129 | 130 | v7 = 7 131 | while True: 132 | v8 = v7 - 7 133 | if v8 >= 7: 134 | break 135 | 136 | v3[v7] = v16[v8] ^ v1[v8] 137 | 138 | v7 += 1 139 | if v7 >= 19: 140 | break 141 | v1_3.clear() 142 | v1_3 += v3[-2:] + v3[:18] 143 | return bytes(v1) 144 | 145 | 146 | def rand_str2(v1_3: bytearray): 147 | v1 = bytearray(10) 148 | memset(v1, 13) 149 | 150 | v3 = bytearray(20) 151 | memset(v3, 13) 152 | memwrite(v3, 0, (16204).to_bytes(2, "little")) 153 | memwrite(v3, 2, (928279336).to_bytes(4, "little")) 154 | memwrite(v3, 6, (610353194).to_bytes(4, "little")) 155 | 156 | v16 = bytearray(10) 157 | memwrite(v16, 0, (24896).to_bytes(2, "little")) 158 | memwrite(v16, 2, (910043961).to_bytes(4, "little")) 159 | memwrite(v16, 6, (1327901016).to_bytes(4, "little")) 160 | 161 | v11 = bytes([97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 111, 112, 113, 114, 115, 116, 117, 162 | 118, 119, 120, 121, 122, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 69, 70, 71, 77, 78]) 163 | v2 = 3 164 | while True: 165 | v1[v2] = v11[random.randint(0, 32767) % 44] 166 | v2 += 1 167 | if v2 > 9: 168 | break 169 | v1[0] = v1[3] 170 | v1[1] = v1[6] 171 | v1[2] = v1[9] 172 | 173 | v7 = 7 174 | while True: 175 | v8 = v7 - 7 176 | if v8 >= 7: 177 | break 178 | 179 | v3[v7] = v16[v8] ^ v1[v8] 180 | 181 | v7 += 1 182 | if v7 >= 19: 183 | break 184 | v1_3.clear() 185 | v1_3 += v3[-2:] + v3[:18] 186 | return bytes(v1) 187 | 188 | 189 | def sub_16F90(data: bytes, v1: bytearray, fix: bytes, v1_3: bytes): 190 | if len(v1) == 0: 191 | v1 += bytearray(rand_str(v1_3)) 192 | 193 | v28 = sha256(data, fix) 194 | v28 += sha512(data, fix) 195 | 196 | return v28[:40] 197 | -------------------------------------------------------------------------------- /core/croto/offical.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | from hashlib import md5 3 | 4 | 5 | def ulong_overflow(val): 6 | MAX = 2147483647 7 | if not -MAX-1 <= val <= MAX: 8 | val = (val + (MAX + 1)) % (2 * (MAX + 1)) - MAX - 1 9 | # return ctypes.c_ulong(val).value 10 | return ctypes.c_uint(val).value 11 | 12 | 13 | def offical(data, key): 14 | eax = int.from_bytes(data[0:4], "big") 15 | esi = int.from_bytes(data[4:8], "big") 16 | 17 | var4 = int.from_bytes(key[0:4][::-1], "big") 18 | ebp = int.from_bytes(key[4:8][::-1], "big") 19 | var3 = int.from_bytes(key[8:12][::-1], "big") 20 | var2 = int.from_bytes(key[12:16][::-1], "big") 21 | 22 | edi = 0x9E3779B9 23 | ecx = edx = 0 24 | 25 | for _ in range(16): 26 | edx = esi 27 | ecx = esi 28 | 29 | edx = ulong_overflow(edx >> 5) 30 | ecx = ulong_overflow(ecx << 4) 31 | 32 | edx = ulong_overflow(edx + ebp) 33 | ecx = ulong_overflow(ecx + var4) 34 | 35 | edx = ulong_overflow(edx ^ ecx) 36 | ecx = ulong_overflow(esi + edi) 37 | 38 | edx = ulong_overflow(edx ^ ecx) 39 | eax = ulong_overflow(eax + edx) 40 | edx = eax 41 | ecx = eax 42 | 43 | edx = ulong_overflow(edx << 4) 44 | edx = ulong_overflow(edx + var3) 45 | ecx = ulong_overflow(ecx >> 5) 46 | ecx = ulong_overflow(ecx + var2) 47 | 48 | edx = ulong_overflow(edx ^ ecx) 49 | ecx = ulong_overflow(eax + edi) 50 | edx = ulong_overflow(edx ^ ecx) 51 | edi = edi - 0x61C88647 52 | esi = ulong_overflow(esi + edx) 53 | 54 | return eax.to_bytes(4, "big") + esi.to_bytes(4, "big") 55 | 56 | 57 | def create_official(bufKey, bufSig, tgt_encrypt): 58 | MD5InfoCount = 4 59 | round = 256 60 | TmOffMod = 19 61 | TmOffModAdd = 5 62 | md5info = md5(bufKey).digest() 63 | md5info = md5info + md5(bufSig).digest() 64 | keyround = 480 % TmOffMod + TmOffModAdd 65 | seq = bytearray(256) 66 | ls = bytearray(256) 67 | off = bytearray(16) 68 | 69 | for i in range(round): 70 | seq[i] = i 71 | ls[i] = md5info[16 + (i % 16)] 72 | 73 | x = 0 74 | for i in range(round): 75 | x = (x + seq[i] + ls[i]) % round 76 | m = seq[x] 77 | seq[x] = seq[i] 78 | seq[i] = m 79 | 80 | x = 0 81 | for i in range(16): 82 | x = (x + seq[i + 1]) % round 83 | m = seq[x] 84 | seq[x] = seq[i + 1] 85 | seq[i + 1] = m 86 | 87 | v = (seq[x] + seq[i + 1]) % round + 1 88 | md5info = md5info + bytes([seq[v - 1] ^ md5info[i]]) 89 | 90 | md5info = md5info + md5(tgt_encrypt).digest() 91 | MD5MD5info = md5(md5info).digest() 92 | M0 = MD5MD5info 93 | for i in range(keyround): 94 | M0 = md5(M0).digest() 95 | md5info = M0 + md5info[16:] 96 | 97 | t1 = MD5MD5info[:8] 98 | t2 = MD5MD5info[8:] 99 | 100 | for i in range(MD5InfoCount): 101 | lp = i * 16 102 | prekey = md5info[lp:] 103 | prekey = prekey[:17] 104 | 105 | KEY = bytes(0) 106 | a = 0 107 | while a < 16: 108 | KEY = KEY + bytes([prekey[a + 3]]) + bytes([prekey[a + 2]]) + \ 109 | bytes([prekey[a + 1]]) + bytes([prekey[a + 0]]) 110 | a += 4 111 | 112 | ii = offical(t1, KEY) + offical(t2, KEY) 113 | b = i 114 | 115 | while b < 16: 116 | off[b] = off[b] ^ ii[b] 117 | b += 1 118 | 119 | return md5(off).digest() 120 | -------------------------------------------------------------------------------- /core/croto/qqtea.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | 4 | 5 | def xor(a, b) -> bytes: 6 | op = 0xffffffff 7 | a1, a2 = struct.unpack(b'>LL', a[0:8]) 8 | b1, b2 = struct.unpack(b'>LL', b[0:8]) 9 | return struct.pack(b'>LL', (a1 ^ b1) & op, (a2 ^ b2) & op) 10 | 11 | 12 | def code(v, k) -> bytes: 13 | n = 16 14 | op = 0xffffffff 15 | delta = 0x9e3779b9 16 | k = struct.unpack(b'>LLLL', k[0:16]) 17 | y, z = struct.unpack(b'>LL', v[0:8]) 18 | s = 0 19 | for _ in range(n): 20 | s += delta 21 | y += (op & (z << 4)) + k[0] ^ z + s ^ (op & (z >> 5)) + k[1] 22 | y &= op 23 | z += (op & (y << 4)) + k[2] ^ y + s ^ (op & (y >> 5)) + k[3] 24 | z &= op 25 | r = struct.pack(b'>LL', y, z) 26 | return r 27 | 28 | 29 | def decipher(v, k): 30 | n = 16 31 | op = 0xffffffff 32 | y, z = struct.unpack(b'>LL', v[0:8]) 33 | a, b, c, d = struct.unpack(b'>LLLL', k[0:16]) 34 | delta = 0x9E3779B9 35 | s = (delta << 4) & op 36 | for _ in range(n): 37 | z -= ((y << 4) + c) ^ (y + s) ^ ((y >> 5) + d) 38 | z &= op 39 | y -= ((z << 4) + a) ^ (z + s) ^ ((z >> 5) + b) 40 | y &= op 41 | s -= delta 42 | s &= op 43 | return struct.pack(b'>LL', y, z) 44 | 45 | 46 | def tea_encrypt(data: bytes, key: bytes): 47 | END_CHAR = b'\0' 48 | FILL_N_OR = 0xF8 49 | vl = len(data) 50 | filln = (8 - (vl + 2)) % 8 + 2 51 | fills = b'' 52 | for i in range(filln): 53 | fills = fills + bytes([220]) 54 | data = (bytes([(filln - 2) | FILL_N_OR]) 55 | + fills 56 | + data 57 | + END_CHAR * 7) 58 | tr = b'\0' * 8 59 | to = b'\0' * 8 60 | r = b'' 61 | o = b'\0' * 8 62 | for i in range(0, len(data), 8): 63 | o = xor(data[i:i + 8], tr) 64 | tr = xor(code(o, key), to) 65 | to = o 66 | r += tr 67 | return r 68 | 69 | def tea_decrypt(data: bytes, key:bytes): 70 | l = len(data) 71 | prePlain = decipher(data, key) 72 | pos = (prePlain[0] & 0x07) + 2 73 | r = prePlain 74 | preCrypt = data[0:8] 75 | for i in range(8, l, 8): 76 | x = xor(decipher(xor(data[i:i + 8], prePlain), key), preCrypt) 77 | prePlain = xor(x, preCrypt) 78 | preCrypt = data[i:i + 8] 79 | r += x 80 | if r[-7:] != b'\0' * 7: 81 | return None 82 | return r[pos + 1:-7] -------------------------------------------------------------------------------- /core/entities/__init__.py: -------------------------------------------------------------------------------- 1 | from .event import ( 2 | NoticeEvent, 3 | MessageEvent, 4 | ) 5 | 6 | from .message import ( 7 | Message, 8 | TextNode, 9 | AtNode, 10 | FaceNode, 11 | ImageNode 12 | ) 13 | 14 | from .packet import ( 15 | Packet, 16 | PacketHandler, 17 | PacketManger 18 | ) 19 | 20 | from .struct import QQStruct -------------------------------------------------------------------------------- /core/entities/event.py: -------------------------------------------------------------------------------- 1 | from .message import Message 2 | 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass 7 | class BaseEvent: 8 | time: int = 0 9 | 10 | self_id: int = 0 11 | 12 | post_type: str = "" 13 | 14 | 15 | @dataclass 16 | class MessageEvent(BaseEvent): 17 | post_type: str = "message" 18 | 19 | message_type: str = "" 20 | 21 | sub_type: str = "" 22 | 23 | message_id: int = 0 24 | 25 | message_num: int = 0 26 | 27 | group_id: int = 0 28 | 29 | user_id: int = 0 30 | 31 | message: Message = None 32 | 33 | 34 | @dataclass 35 | class NoticeEvent(BaseEvent): 36 | post_type: str = "notice" 37 | 38 | notice_type: str = "" 39 | 40 | sub_type: str = "" 41 | 42 | group_id: int = 0 43 | 44 | operator_id: int = 0 45 | 46 | user_id: int = 0 47 | -------------------------------------------------------------------------------- /core/entities/message.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | from dataclasses import dataclass 3 | from abc import ABCMeta, abstractmethod 4 | 5 | from core import utils 6 | 7 | 8 | class BaseNode(metaclass=ABCMeta): 9 | @abstractmethod 10 | def format(self) -> str: 11 | """将该 Node 格式化为字符串""" 12 | pass 13 | 14 | @abstractmethod 15 | def encode(self) -> bytes: 16 | """将该 Node 编码成对应的协议源数据""" 17 | pass 18 | 19 | @abstractmethod 20 | def from_raw(self, raw_data: bytes): 21 | """解析协议源数据并返回对应的 Node 对象""" 22 | pass 23 | 24 | 25 | @dataclass 26 | class TextNode(BaseNode): 27 | text: str 28 | 29 | def format(self) -> str: 30 | return self.text 31 | 32 | def encode(self) -> bytes: 33 | stream = utils.Stream() 34 | body = self.text.encode() 35 | 36 | stream.write_byte(0x01) 37 | stream.write_int16(len(body) + 3) 38 | stream.write_byte(0x01) 39 | stream.write_int16(len(body)) 40 | stream.write(body) 41 | 42 | return stream.read_all() 43 | 44 | def from_raw(raw_data: bytes): 45 | stream = utils.Stream(raw_data) 46 | text = stream.del_left(1).read_token().decode() 47 | 48 | if len(stream._raw) > 0: 49 | return AtNode.from_raw(raw_data) 50 | 51 | return TextNode(text) 52 | 53 | 54 | @dataclass 55 | class AtNode(BaseNode): 56 | uin: int 57 | name: str 58 | 59 | def format(self) -> str: 60 | return f"[PQ:at,qq={self.uin},name={self.name}]" 61 | 62 | def encode(self) -> bytes: 63 | stream = utils.Stream() 64 | name = "@" + self.name 65 | 66 | stream.write_hex("00 01 00 00") 67 | stream.write_int16(len(name)) 68 | stream.write_hex("00") 69 | stream.write_int32(self.uin) 70 | stream.write_hex("00 00") 71 | body = stream.read_all() 72 | 73 | name = name.encode() 74 | stream.write_byte(0x01) 75 | stream.write_int16(len(name)) 76 | stream.write(name) 77 | stream.write_byte(0x06) 78 | stream.write_int16(len(body)) 79 | stream.write(body) 80 | body = stream.read_all() 81 | 82 | stream.write_byte(0x01) 83 | stream.write_int16(len(body)) 84 | stream.write(body) 85 | 86 | return stream.read_all() 87 | 88 | def from_raw(raw_data: bytes): 89 | stream = utils.Stream(raw_data) 90 | 91 | stream.del_left(1) 92 | name = stream.read_token().decode()[1:] 93 | 94 | stream.del_left(10) 95 | uin = stream.read_int32() 96 | 97 | return AtNode(uin, name) 98 | 99 | 100 | @dataclass 101 | class FaceNode(BaseNode): 102 | id: int 103 | 104 | def format(self) -> str: 105 | return f"[PQ:face,id={self.id}]" 106 | 107 | def encode(self) -> bytes: 108 | stream = utils.Stream() 109 | 110 | stream.write_byte(0x02) 111 | stream.write_int16(1 + 3) 112 | stream.write_byte(0x01) 113 | stream.write_int16(1) 114 | stream.write_byte(self.id) 115 | 116 | return stream.read_all() 117 | 118 | def from_raw(raw_data: bytes): 119 | stream = utils.Stream(raw_data) 120 | face_id = stream.del_left(3).read_byte() 121 | 122 | return FaceNode(face_id) 123 | 124 | 125 | @dataclass 126 | class ImageNode(BaseNode): 127 | hash: str 128 | 129 | def format(self) -> str: 130 | return f"[PQ:image,url=https://gchat.qpic.cn/gchatpic_new/0/0-0-{self.hash}/0?term=3]" 131 | 132 | def encode(self) -> bytes: 133 | return b'' 134 | 135 | def from_raw(raw_data: bytes): 136 | stream = utils.Stream(raw_data) 137 | 138 | stream.del_left(1) 139 | uuid = stream.read_token().decode().strip(" {}") 140 | hash = uuid.replace("-", "").upper()[:32] 141 | 142 | return ImageNode(hash) 143 | 144 | 145 | MessageNode = Union[TextNode, AtNode, FaceNode, ImageNode] 146 | 147 | 148 | class Message: 149 | def __init__(self): 150 | self.__nodes: List[MessageNode] = [] 151 | 152 | def add(self, node: MessageNode): 153 | if isinstance(node, TextNode): 154 | pass 155 | elif isinstance(node, AtNode): 156 | pass 157 | elif isinstance(node, FaceNode): 158 | pass 159 | elif isinstance(node, ImageNode): 160 | pass 161 | else: 162 | raise ValueError("Message 添加的元素不是 MessageNode 包括的类型") 163 | 164 | self.__nodes.append(node) 165 | return self 166 | 167 | def format(self): 168 | format = "" 169 | 170 | for node in self.__nodes: 171 | format += node.format() 172 | return format 173 | 174 | def encode(self): 175 | raw_data = b'' 176 | for node in self.__nodes: 177 | raw_data += node.encode() 178 | return raw_data 179 | 180 | def from_raw(raw_data: bytes): 181 | message = Message() 182 | stream = utils.Stream(raw_data) 183 | 184 | while len(stream._raw) > 3: 185 | node_type = stream.read_byte() 186 | node_raw = stream.read_token() 187 | 188 | if node_type == 0x01: # 文本消息 189 | message.add(TextNode.from_raw(node_raw)) 190 | elif node_type == 0x02: # 表情消息 191 | message.add(FaceNode.from_raw(node_raw)) 192 | elif node_type == 0x03: # 群图片消息 193 | message.add(ImageNode.from_raw(node_raw)) 194 | elif node_type == 0x06: # 私聊图片消息 195 | message.add(ImageNode.from_raw(node_raw)) 196 | 197 | return message -------------------------------------------------------------------------------- /core/entities/packet.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, List, Callable, Coroutine 3 | 4 | from core import croto 5 | from core.const import Header, Tail 6 | from core.utils import Stream, rand_bytes 7 | 8 | 9 | @dataclass 10 | class Packet: 11 | header: str = Header 12 | 13 | cmd: str = "" 14 | 15 | sequence: bytes = rand_bytes(2) 16 | 17 | uin: int = 0 18 | 19 | version: Stream = Stream() 20 | 21 | body: Stream = Stream() 22 | 23 | tail: str = Tail 24 | 25 | tea_key: bytes = b'' 26 | 27 | def encode(self): 28 | stream = Stream() 29 | 30 | stream.write_hex(self.header) 31 | 32 | stream.write_hex(self.cmd) 33 | stream.write(self.sequence) 34 | stream.write_int32(self.uin) 35 | 36 | stream.write_stream(self.version) 37 | if self.tea_key == b'': 38 | stream.write_stream(self.body) 39 | else: 40 | data = self.body.read_all() 41 | stream.write(croto.tea_encrypt(data, self.tea_key)) 42 | 43 | stream.write_hex(self.tail) 44 | 45 | return stream.read_all() 46 | 47 | def from_raw(raw_data: bytes, tea_key: bytes): 48 | stream = Stream(raw_data) 49 | stream.del_left(3).del_right(1) 50 | 51 | packet = Packet( 52 | cmd=stream.read_hex(2), 53 | sequence=stream.read(2), 54 | uin=stream.read_int32(), 55 | tea_key=tea_key 56 | ) 57 | 58 | stream.del_left(3) 59 | if packet.tea_key == b'': 60 | packet.body = stream 61 | else: 62 | data = croto.tea_decrypt(stream._raw, tea_key) 63 | packet.body = Stream(data) 64 | 65 | return packet 66 | 67 | 68 | @dataclass 69 | class PacketHandler: 70 | temp: bool 71 | 72 | check: Callable[[Packet], bool] 73 | 74 | handle: Callable[[Packet], Coroutine[Any, Any, None]] 75 | 76 | 77 | class PacketManger(List[PacketHandler]): 78 | def __repr__(self) -> str: 79 | return "PacketManger{\n%s\n}" % ("\n\n".join(["\t" + str(phr) for phr in self])) 80 | 81 | def append(self, phr: PacketHandler) -> None: 82 | if not isinstance(phr, PacketHandler): 83 | raise ValueError("PacketManger 添加的元素不是 PacketHandler 类型") 84 | 85 | if not phr in self: 86 | super().append(phr) 87 | 88 | return self 89 | 90 | def pop(self, phr: PacketHandler): 91 | if phr in self: 92 | super().pop(self.index(phr)) 93 | 94 | return self 95 | 96 | async def exec_all(self, packet: Packet): 97 | for phr in self: 98 | if not phr.check(packet): 99 | continue 100 | 101 | if phr.temp: 102 | self.pop(phr) 103 | await phr.handle(packet) 104 | -------------------------------------------------------------------------------- /core/entities/struct.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from core import croto 4 | 5 | @dataclass 6 | class QQStruct: 7 | addr: tuple = () 8 | 9 | local_ip: bytes = b'' 10 | 11 | server_ip: bytes = b'' 12 | 13 | uin: int = 0 14 | 15 | password: bytes = b'' 16 | 17 | nickname: str = '' 18 | 19 | is_scancode = True 20 | 21 | is_running = False 22 | 23 | login_time: bytes = b'' 24 | 25 | ecdh: croto.ECDH = None 26 | 27 | path: str = "" 28 | 29 | redirection_times: int = 0 30 | 31 | redirection_history: bytes = b'' 32 | 33 | tgt_key: bytes = b'' 34 | 35 | tgtgt_key: bytes = b'' 36 | 37 | session_key: bytes = b'' 38 | 39 | offical: bytes = b'' 40 | 41 | bkn: int = 0 42 | 43 | skey: str = '' 44 | 45 | cookie: str = '' 46 | 47 | pckey_for_0819: bytes = b'' 48 | 49 | pckey_for_0828_send: bytes = b'' 50 | 51 | pckey_for_0828_recv: bytes = b'' 52 | 53 | token_by_scancode: bytes = b'' 54 | 55 | token_0038_from_0825: bytes = b'' 56 | 57 | token_0038_from_0818: bytes = b'' 58 | 59 | token_0038_from_0836: bytes = b'' 60 | 61 | token_0088_from_0836: bytes = b'' -------------------------------------------------------------------------------- /core/protocol/event/__init__.py: -------------------------------------------------------------------------------- 1 | from .group_msg import handle_group_msg 2 | from .msg_receipt import handle_msg_receipt 3 | from .private_msg import handle_private_msg -------------------------------------------------------------------------------- /core/protocol/event/group_msg.py: -------------------------------------------------------------------------------- 1 | from typing import Coroutine, Callable 2 | from logger import logger 3 | 4 | from core.protocol import webapi 5 | from core.client import QQClient 6 | from core.entities import ( 7 | Packet, 8 | PacketHandler, 9 | MessageEvent, 10 | Message, 11 | ) 12 | 13 | from .msg_receipt import pack_0002_receipt 14 | 15 | def handle_group_msg(cli: QQClient, func: Callable[[Message], Coroutine]): 16 | def check_0017(packet: Packet): 17 | if packet.cmd != "00 17": 18 | return False 19 | 20 | return packet.body._raw[18:20] == bytes.fromhex("00 52") 21 | 22 | async def handle_0017(packet: Packet): 23 | event = MessageEvent( 24 | message_type="group", 25 | sub_type="normal" 26 | ) 27 | 28 | body = packet.body 29 | guid = body.read_int32() 30 | event.self_id = body.read_int32() 31 | 32 | body.del_left(12) 33 | body.read(body.read_int32()) 34 | 35 | event.group_id = body.read_int32() 36 | event.user_id = body.del_left(1).read_int32() 37 | 38 | event.message_id = body.read_int32() 39 | event.time = body.read_int32() 40 | 41 | body.del_left(8) 42 | body.del_left(1).del_left(1).del_left(2) 43 | body.del_left(12) 44 | 45 | send_time = body.read_int32() 46 | event.message_num = body.read_int32() 47 | 48 | body.del_left(8).read_token() 49 | body.del_left(2) 50 | event.raw_message = body._raw 51 | 52 | event.message = Message.from_raw(body.read_all()) 53 | 54 | group = await webapi.get_group(cli, event.group_id) 55 | member = group.members[str(event.user_id)] 56 | cli.send(pack_0002_receipt(cli, event)) 57 | 58 | logger.info(f"收到群聊 {group.name}({event.group_id})内 {member.card}({event.user_id})消息: {event.message.format()}") 59 | cli.add_task(func(event)) 60 | 61 | return PacketHandler( 62 | temp=False, 63 | check=check_0017, 64 | handle=handle_0017 65 | ) 66 | -------------------------------------------------------------------------------- /core/protocol/event/msg_receipt.py: -------------------------------------------------------------------------------- 1 | from core import const, croto 2 | from core.client import QQClient 3 | from core.entities import ( 4 | Packet, 5 | PacketHandler, 6 | MessageEvent, 7 | ) 8 | 9 | 10 | def pack_0002_receipt(cli: QQClient, event: MessageEvent): 11 | packet = Packet( 12 | cmd="00 02", 13 | uin=cli.stru.uin, 14 | tea_key=cli.stru.session_key 15 | ) 16 | 17 | packet.version.write(const.BodyVersion) 18 | packet.body.write_byte(41) 19 | packet.body.write_int32(croto.gid_from_group(event.group_id)) 20 | packet.body.write_byte(2) 21 | packet.body.write_int32(event.message_id) 22 | 23 | return packet 24 | 25 | 26 | def pack_0319_receipt(cli: QQClient, event: MessageEvent): 27 | packet = Packet( 28 | cmd="03 19", 29 | uin=cli.stru.uin, 30 | tea_key=cli.stru.session_key 31 | ) 32 | 33 | packet.version.write(const.FuncVersion) 34 | 35 | packet.body.write_hex("08 01") 36 | packet.body.write_hex("12 03 98 01 00") 37 | packet.body.write_hex("0A 0E 08") 38 | packet.body.write_varint(event.user_id) 39 | packet.body.write_hex("10") 40 | packet.body.write_varint(event.time) 41 | packet.body.write_hex("20 00") 42 | data = packet.body.read_all() 43 | 44 | packet.body.write_hex("00 00 00 07") 45 | packet.body.write_int32(len(data)-7) 46 | packet.body.write(data) 47 | 48 | return packet 49 | 50 | 51 | def handle_msg_receipt(cli: QQClient): 52 | def check_0017_00ce(packet: Packet): 53 | return packet.cmd in ["00 17", "00 CE", "03 55"] 54 | 55 | async def handle_0017_00ce(packet: Packet): 56 | receipt_packet = Packet( 57 | cmd=packet.cmd, 58 | uin=cli.stru.uin, 59 | sequence=packet.sequence, 60 | tea_key=cli.stru.session_key 61 | ) 62 | 63 | receipt_packet.version.write(const.BodyVersion) 64 | receipt_packet.body.write(packet.body._raw[0:16]) 65 | cli.send(receipt_packet) 66 | 67 | return PacketHandler( 68 | temp=False, 69 | check=check_0017_00ce, 70 | handle=handle_0017_00ce 71 | ) 72 | -------------------------------------------------------------------------------- /core/protocol/event/private_msg.py: -------------------------------------------------------------------------------- 1 | from typing import Coroutine, Callable 2 | from logger import logger 3 | 4 | from core.protocol import webapi 5 | from core.client import QQClient 6 | from core.entities import ( 7 | Packet, 8 | PacketHandler, 9 | MessageEvent, 10 | Message 11 | ) 12 | 13 | from .msg_receipt import pack_0319_receipt 14 | 15 | 16 | def handle_private_msg(cli: QQClient, func: Callable[[Message], Coroutine]): 17 | def check_00ce(packet: Packet): 18 | if packet.cmd != "00 CE": 19 | return False 20 | 21 | return packet.body._raw[18:20] == bytes.fromhex("00 A6") 22 | 23 | async def handle_00ce(packet: Packet): 24 | event = MessageEvent( 25 | message_type="private", 26 | sub_type="normal" 27 | ) 28 | 29 | body = packet.body 30 | event.user_id = body.read_int32() 31 | event.self_id = body.read_int32() 32 | 33 | body.del_left(12) 34 | body.del_left(body.read_int32() + 26) 35 | 36 | if body.read_hex(2) == "00 AF": 37 | pass # 好友抖动 38 | 39 | event.message_id = body.read_int16() 40 | event.time = body.read_int32() 41 | 42 | body.del_left(6).del_left(1).del_left(1) 43 | body.del_left(2).del_left(9) 44 | 45 | send_time = body.read_int32() 46 | event.message_num = body.read_int32() 47 | 48 | body.del_left(8).read_token() 49 | body.read_int16() 50 | 51 | event.message = Message.from_raw(body.read_all()) 52 | user = await webapi.get_user(cli, event.user_id) 53 | 54 | cli.send(pack_0319_receipt(cli, event)) 55 | 56 | logger.info( 57 | f"收到私聊 {user.name}({event.user_id}) 消息: {event.message.format()}") 58 | cli.add_task(func(event)) 59 | 60 | return PacketHandler( 61 | temp=False, 62 | check=check_00ce, 63 | handle=handle_00ce 64 | ) 65 | -------------------------------------------------------------------------------- /core/protocol/pcapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .send_msg import send_group_msg, send_private_msg -------------------------------------------------------------------------------- /core/protocol/pcapi/send_msg.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | 3 | from core import const, utils, croto 4 | from core.client import QQClient 5 | from core.entities import Packet, Message 6 | 7 | def send_group_msg(cli: QQClient, group_id:int, message: Message): 8 | packet = Packet( 9 | cmd="00 02", 10 | uin=cli.stru.uin, 11 | tea_key=cli.stru.session_key 12 | ) 13 | packet.version.write(const.BodyVersion) 14 | 15 | body = message.encode() 16 | timestamp = int(time()).to_bytes(4, 'big') 17 | 18 | packet.body.write_hex("00 01 01 00 00 00 00 00 00 00") 19 | packet.body.write_hex("4D 53 47 00 00 00 00 00") 20 | 21 | packet.body.write(timestamp) 22 | packet.body.write(utils.rand_bytes(4)) 23 | 24 | packet.body.write_hex("00 00 00 00 09 00 86 00 00") 25 | packet.body.write_hex("06 E5 AE 8B E4 BD 93 00 00") 26 | packet.body.write(body) 27 | body = packet.body.read_all() 28 | 29 | packet.body.write_hex("2A") 30 | packet.body.write_int32(croto.gid_from_group(group_id)) 31 | packet.body.write_int16(len(body)) 32 | packet.body.write(body) 33 | cli.send(packet) 34 | 35 | def send_private_msg(cli: QQClient, user_id:int, message: Message): 36 | packet = Packet( 37 | cmd="00 CD", 38 | uin=cli.stru.uin, 39 | tea_key=cli.stru.session_key 40 | ) 41 | packet.version.write_hex("03 00 00 00 01 01 01 00 00 6A 9C 75 37 7D 94") 42 | 43 | msg_data = message.encode() 44 | timestamp = int(time()).to_bytes(4, "big") 45 | 46 | packet.body.write_int32(cli.stru.uin) 47 | packet.body.write_int32(user_id) 48 | 49 | packet.body.write_hex("00 00 00 08 00 01 00 04 00 00 00 00") 50 | packet.body.write(const.VMainVer) 51 | 52 | packet.body.write_int32(cli.stru.uin) 53 | packet.body.write_int32(user_id) 54 | packet.body.write(croto.md5(user_id.to_bytes(4, "big") + cli.stru.session_key)) 55 | 56 | packet.body.write_hex("00 0B") 57 | packet.body.write(utils.rand_bytes(2)) 58 | packet.body.write(timestamp) 59 | 60 | packet.body.write_hex("00 00 00 00 00 00 01 00 00 00 01 4D 53 47 00 00 00 00 00") 61 | packet.body.write(timestamp) 62 | packet.body.write(utils.rand_bytes(4)) 63 | packet.body.write_hex("00 00 00 00 09 00 86 00 00 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 00 00") 64 | packet.body.write(msg_data) 65 | cli.send(packet) -------------------------------------------------------------------------------- /core/protocol/webapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .custom_api import get_user, get_group 2 | 3 | from .offical_api import ( 4 | search_v3, 5 | get_group_info_all, 6 | get_group_members_new, 7 | set_group_card 8 | ) 9 | -------------------------------------------------------------------------------- /core/protocol/webapi/custom_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, List 3 | from dataclasses import dataclass 4 | from jsondataclass import from_json, to_json 5 | 6 | from core.client import QQClient 7 | from .offical_api import ( 8 | cgi_get_score, 9 | search_v3, 10 | get_group_info_all, 11 | get_group_members_new, 12 | ) 13 | 14 | 15 | @dataclass 16 | class QQUser: 17 | name: str 18 | sex: str 19 | area: str 20 | 21 | 22 | @dataclass 23 | class QQMember: 24 | name: str 25 | card: str 26 | role: str 27 | 28 | 29 | @dataclass 30 | class QQGroup: 31 | name: str 32 | owner: int 33 | admins: List[int] 34 | members: Dict[str, QQMember] 35 | 36 | 37 | QQUserCache: Dict[str, QQUser] = {} 38 | QQGroupCache: Dict[str, QQGroup] = {} 39 | 40 | 41 | async def get_user(cli: QQClient, user_id: int) -> QQUser: 42 | global QQUserCache 43 | user_id = str(user_id) 44 | path = os.path.join(cli.stru.path, "QQUser.json") 45 | 46 | if QQUserCache == {} and os.path.exists(path): 47 | with open(path, "r", encoding="utf-8") as f: 48 | QQUserCache = from_json(f.read(), Dict[str, QQUser]) 49 | 50 | 51 | if user_id in QQUserCache: 52 | return QQUserCache[user_id] 53 | 54 | ret = await search_v3(cli.stru, user_id) 55 | if ret == None: 56 | ret = await cgi_get_score(user_id) 57 | QQUserCache[user_id] = QQUser( 58 | name=ret[user_id][-4], 59 | sex="unknow", 60 | area="", 61 | ) 62 | else: 63 | QQUserCache[user_id] = QQUser( 64 | name=ret["nick"], 65 | sex={1: "male", 2: "female"}.get(ret["gender"], "unknow"), 66 | area=ret["country"], 67 | ) 68 | 69 | with open(path, "w", encoding="utf-8") as f: 70 | f.write(to_json(QQUserCache, ensure_ascii=False)) 71 | 72 | return QQUserCache[user_id] 73 | 74 | 75 | async def get_group(cli: QQClient, group_id: int): 76 | global QQUserCache, QQGroupCache 77 | group_id = str(group_id) 78 | path = os.path.join(cli.stru.path, f"{group_id}.json") 79 | 80 | if not group_id in QQGroupCache and os.path.exists(path): 81 | with open(path, "r", encoding="utf-8") as f: 82 | QQGroupCache[group_id] = from_json(f.read(), QQGroup) 83 | 84 | if group_id in QQGroupCache: 85 | return QQGroupCache[group_id] 86 | 87 | ret = await get_group_info_all(cli.stru, group_id) 88 | group = QQGroup( 89 | name=ret["gName"], 90 | owner=ret["gOwner"], 91 | admins=ret["gAdmins"], 92 | members=dict() 93 | ) 94 | 95 | ret = await get_group_members_new(cli.stru, group_id) 96 | for mem in ret["mems"]: 97 | if mem['u'] == group.owner: 98 | mem_role = "owner" 99 | elif mem['u'] in group.admins: 100 | mem_role = "admin" 101 | else: 102 | mem_role = "member" 103 | 104 | group.members[str(mem['u'])] = QQMember( 105 | name=mem["n"], 106 | card=ret['cards'].get(str(mem['u']), mem["n"]), 107 | role=mem_role, 108 | ) 109 | 110 | QQGroupCache[group_id] = group 111 | with open(path, "w", encoding="utf-8") as f: 112 | f.write(to_json(QQGroupCache[group_id], ensure_ascii=False)) 113 | 114 | return QQGroupCache[group_id] 115 | -------------------------------------------------------------------------------- /core/protocol/webapi/offical_api.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import json 3 | 4 | from core.entities import QQStruct 5 | 6 | async def cgi_get_score(user_id:int) -> dict: 7 | async with httpx.AsyncClient() as client: 8 | rsp = await client.get(f"https://r.qzone.qq.com/fcg-bin/cgi_get_score.fcg?mask=7&uins={user_id}") 9 | 10 | return json.loads(rsp.text.strip('portraitCallBack();')) 11 | 12 | async def search_v3(stru: QQStruct, user_id: int): 13 | async with httpx.AsyncClient() as client: 14 | rsp = await client.get( 15 | url="https://cgi.find.qq.com/qqfind/buddy/search_v3", 16 | params={"keyword": user_id}, 17 | headers={"Cookie": stru.cookie} 18 | ) 19 | 20 | try: 21 | ret = rsp.json() 22 | return ret["result"]["buddy"]["info_list"][0] 23 | except: 24 | return None 25 | 26 | 27 | async def get_group_info_all(stru: QQStruct, group_id: int): 28 | async with httpx.AsyncClient() as client: 29 | rsp = await client.get( 30 | url="https://qinfo.clt.qq.com/cgi-bin/qun_info/get_group_info_all", 31 | params={ 32 | "gc": group_id, 33 | "bkn": stru.bkn, 34 | "src": "qinfo_v3", 35 | }, 36 | headers={"Cookie": stru.cookie} 37 | ) 38 | 39 | return rsp.json() 40 | 41 | 42 | async def get_group_members_new(stru: QQStruct, group_id: int): 43 | async with httpx.AsyncClient() as client: 44 | rsp = await client.get( 45 | url="https://qinfo.clt.qq.com/cgi-bin/qun_info/get_group_members_new", 46 | params={ 47 | "gc": group_id, 48 | "bkn": stru.bkn, 49 | "src": "qinfo_v3", 50 | }, 51 | headers={"Cookie": stru.cookie} 52 | ) 53 | 54 | return rsp.json() 55 | 56 | # Success: {'ec': 0, 'errcode': 0, 'em': 'ok'} 57 | # Failed: {'ec': 3, 'errcode': 0, 'em': ''} 58 | 59 | 60 | async def set_group_card(stru: QQStruct, group_id: int, user_id: int, card: str): 61 | async with httpx.AsyncClient() as client: 62 | rsp = await client.post( 63 | url="https://qinfo.clt.qq.com/cgi-bin/qun_info/set_group_card", 64 | data={ 65 | "u": user_id, 66 | "gc": group_id, 67 | "bkn": stru.bkn, 68 | "src": "qinfo_v3", 69 | "name": card 70 | }, 71 | headers={"Cookie": stru.cookie} 72 | ) 73 | 74 | return rsp.json() 75 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/__init__.py: -------------------------------------------------------------------------------- 1 | from .wtlogin import login_out, login_by_scan, login_by_token 2 | 3 | __all__ = ["login_out", "login_by_scan", "login_by_token"] -------------------------------------------------------------------------------- /core/protocol/wtlogin/p1_0825_ping/__init__.py: -------------------------------------------------------------------------------- 1 | from .pack import pack_0825 2 | from .unpack import unpack_0825 -------------------------------------------------------------------------------- /core/protocol/wtlogin/p1_0825_ping/pack.py: -------------------------------------------------------------------------------- 1 | from core import utils 2 | from core import const 3 | from core.entities import Packet, QQStruct 4 | 5 | 6 | def pack_0825(stru: QQStruct): 7 | packet = Packet( 8 | cmd="08 25", 9 | uin=stru.uin, 10 | tea_key=const.RandKey 11 | ) 12 | packet.version.write(const.StructVersion).write(const.RandKey) 13 | 14 | def tlv_018(ns: utils.Stream): 15 | ns.write_hex("00 01") 16 | ns.write(const.SsoVersion) 17 | ns.write(const.ServiceId) 18 | ns.write(const.ClientVersion) 19 | ns.write_int32(stru.uin) 20 | ns.write_int16(stru.redirection_times) 21 | ns.write_hex("00 00") 22 | 23 | ns.write_tlv("00 18", ns.read_all()) 24 | packet.body.write_func(tlv_018) 25 | 26 | def tlv_309(ns: utils.Stream): 27 | ns.write_hex("00 0A 00 04") 28 | ns.write_hex("00 00 00 00") 29 | 30 | if stru.redirection_times > 0: 31 | ns.write_byte(stru.redirection_times) 32 | ns.write_hex("00 04") 33 | ns.write(stru.redirection_history) 34 | ns.write_hex("00 04") 35 | 36 | ns.write_tlv("03 09", ns.read_all()) 37 | packet.body.write_func(tlv_309) 38 | 39 | def tlv_036(ns: utils.Stream): 40 | ns.write_hex("00 02 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00") 41 | 42 | ns.write_tlv("00 36", ns.read_all()) 43 | packet.body.write_func(tlv_036) 44 | 45 | def tlv_114(ns: utils.Stream): 46 | ns.write(const.EcdhVersion) 47 | 48 | ns.write_token(stru.ecdh.public_key) 49 | 50 | ns.write_tlv("01 14", ns.read_all()) 51 | packet.body.write_func(tlv_114) 52 | 53 | def tlv_511(ns: utils.Stream): 54 | ns.write_hex("0A 00 00 00 00 01") 55 | 56 | ns.write_tlv("05 11", ns.read_all()) 57 | packet.body.write_func(tlv_511) 58 | 59 | return packet 60 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/p1_0825_ping/unpack.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from logger import logger 4 | from core.entities import QQStruct, Packet, PacketHandler 5 | 6 | 7 | def check_0825(packet: Packet): 8 | return packet.cmd == "08 25" 9 | 10 | 11 | async def handle_0825(stru: QQStruct, packet: Packet): 12 | stream = packet.body 13 | 14 | sign = stream.read_byte() 15 | stream.read(2) 16 | stru.token_0038_from_0825 = stream.read(stream.read_int16()) 17 | stream.read(6) 18 | stru.login_time = stream.read(4) 19 | stru.local_ip = stream.read(4) 20 | stream.read(2) 21 | 22 | logger.info(f"已选用 {stru.addr[0]} 作为登录服务器") 23 | if sign == 0xfe: 24 | stream.read(18) 25 | stru.server_ip = stream.read(4) 26 | stru.redirection_times += 1 27 | stru.redirection_history += stru.server_ip 28 | elif sign == 0x00: 29 | stream.read(6) 30 | stru.server_ip = stream.read(4) 31 | stru.redirection_history = b'' 32 | 33 | 34 | def unpack_0825(stru: QQStruct): 35 | return PacketHandler( 36 | temp=True, 37 | check=check_0825, 38 | handle=partial(handle_0825, stru) 39 | ) 40 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/p2_0818_qrcode/__init__.py: -------------------------------------------------------------------------------- 1 | from .pack import pack_0818 2 | from .unpack import unpack_0818 -------------------------------------------------------------------------------- /core/protocol/wtlogin/p2_0818_qrcode/pack.py: -------------------------------------------------------------------------------- 1 | from core import utils 2 | from core import const 3 | from core.entities import Packet, QQStruct 4 | 5 | 6 | def pack_0818(stru: QQStruct): 7 | packet = Packet( 8 | cmd="08 18", 9 | tea_key=const.RandKey 10 | ) 11 | packet.version.write(const.StructVersion).write(const.RandKey) 12 | 13 | def tlv_019(ns: utils.Stream): 14 | ns.write_hex("00 01") 15 | ns.write(const.SsoVersion) 16 | ns.write(const.ServiceId) 17 | ns.write(const.ClientVersion) 18 | ns.write_hex("00 00") 19 | 20 | ns.write_tlv("00 19", ns.read_all()) 21 | packet.body.write_func(tlv_019) 22 | 23 | def tlv_114(ns: utils.Stream): 24 | ns.write(const.EcdhVersion) 25 | ns.write_token(stru.ecdh.public_key) 26 | 27 | ns.write_tlv("01 14", ns.read_all()) 28 | packet.body.write_func(tlv_114) 29 | 30 | packet.body.write_hex("03 05 00 1E 00 00 00 00 00 00 00 05 00 00 00 04 00") 31 | packet.body.write_hex("00 00 00 00 00 00 48 00 00 00 02 00 00 00 02 00 00") 32 | packet.body.write_hex("00 15 00 30 00 01 01 45 AA 72 E9 00 10") 33 | packet.body.write_hex("6B 10 A0 47 00 00 00 00 00 00 00 00 00") 34 | packet.body.write_hex("00 00 00 02 9A 76 CB 8F 00 10 D4 61 6E") 35 | packet.body.write_hex("EB D5 B6 4E 2C 5B 6C FA C3 E0 FD 53 90") 36 | 37 | return packet 38 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/p2_0818_qrcode/unpack.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from logger import logger 4 | from core.utils import QRCode 5 | from core.entities import QQStruct, Packet, PacketHandler 6 | 7 | 8 | def check_0818(packet: Packet): 9 | return packet.cmd == "08 18" 10 | 11 | 12 | async def handle_0818(stru: QQStruct, packet: Packet): 13 | if packet.body.read_byte() != 0x00: 14 | logger.fatal("登录二维码获取失败,请尝试重新运行") 15 | raise RuntimeError("Can't get login qrcode") 16 | 17 | stru.pckey_for_0819 = packet.body.del_left(6).read(16) 18 | stru.token_0038_from_0818 = packet.body.del_left(4).read_token(True) 19 | stru.token_by_scancode = packet.body.del_left(4).read_token(True) 20 | 21 | packet.body.del_left(4) 22 | qrcode = QRCode(stru.path, packet.body.read_token()) 23 | logger.info('登录二维码获取成功,已保存至' + qrcode.path) 24 | qrcode.auto_show() 25 | 26 | 27 | def unpack_0818(stru: QQStruct): 28 | return PacketHandler( 29 | temp=True, 30 | check=check_0818, 31 | handle=partial(handle_0818, stru) 32 | ) 33 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/p3_0819_scan/__init__.py: -------------------------------------------------------------------------------- 1 | from .pack import pack_0819 2 | from .unpack import unpack_0819 -------------------------------------------------------------------------------- /core/protocol/wtlogin/p3_0819_scan/pack.py: -------------------------------------------------------------------------------- 1 | from core import utils 2 | from core import const 3 | from core.entities import Packet, QQStruct 4 | 5 | 6 | def pack_0819(stru: QQStruct): 7 | packet = Packet( 8 | cmd="08 19", 9 | uin=stru.uin, 10 | tea_key=stru.pckey_for_0819 11 | ) 12 | packet.version.write(const.StructVersion) 13 | packet.version.write_hex("00 30").write_token(stru.token_0038_from_0818) 14 | 15 | def tlv_019(ns: utils.Stream): 16 | ns.write_hex("00 01") 17 | ns.write(const.SsoVersion) 18 | ns.write(const.ServiceId) 19 | ns.write(const.ClientVersion) 20 | ns.write_hex("00 00") 21 | 22 | ns.write_tlv("00 19", ns.read_all()) 23 | packet.body.write_func(tlv_019) 24 | 25 | packet.body.write_hex("03 01").write_token(stru.token_by_scancode) 26 | return packet 27 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/p3_0819_scan/unpack.py: -------------------------------------------------------------------------------- 1 | import os 2 | from functools import partial 3 | 4 | from logger import logger 5 | from core.entities import QQStruct, Packet, PacketHandler 6 | 7 | 8 | def check_0819(packet: Packet): 9 | return packet.cmd == "08 19" 10 | 11 | 12 | async def handle_0819(stru: QQStruct, packet: Packet): 13 | state = packet.body.read_byte() 14 | 15 | if state == 0x01: 16 | logger.info(f'账号 {packet.uin} 已扫码,请在手机上确认登录') 17 | elif state == 0x00: 18 | stru.uin = packet.uin 19 | stru.password = packet.body.del_left(2).read_token() 20 | stru.tgt_key = packet.body.del_left(2).read_token() 21 | 22 | path = os.path.join(stru.path, "QrCode.jpg") 23 | if os.path.exists(path): 24 | os.remove(path) 25 | 26 | logger.info(f'账号 {packet.uin} 已确认登录, 尝试登录中......') 27 | 28 | 29 | def unpack_0819(stru: QQStruct): 30 | return PacketHandler( 31 | temp=True, 32 | check=check_0819, 33 | handle=partial(handle_0819, stru) 34 | ) 35 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/p4_0836_login/__init__.py: -------------------------------------------------------------------------------- 1 | from .pack import pack_0836 2 | from .unpack import unpack_0836 -------------------------------------------------------------------------------- /core/protocol/wtlogin/p4_0836_login/pack.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from binascii import crc32 3 | 4 | from core import utils, croto 5 | from logger import logger 6 | from core import const 7 | from core.entities import Packet, QQStruct 8 | 9 | private_0836key = const.RandKey 10 | bufOfficiaKey = utils.rand_bytes(16) 11 | OfficialData = utils.rand_bytes(56) 12 | 13 | def pack_0836(stru: QQStruct): 14 | packet = Packet(cmd="08 36", tea_key=stru.ecdh.share_key, uin=stru.uin) 15 | packet.version.write(const.StructVersion).write_hex("00 02") 16 | packet.version.write(const.EcdhVersion) 17 | packet.version.write_token(stru.ecdh.public_key).write_hex("00 00") 18 | packet.version.write_token(private_0836key) 19 | 20 | computer_id = croto.md5(f"{stru.uin}ComputerID".encode()) 21 | computer_id_ex = croto.md5(f"{stru.uin}ComputerIDEx".encode()) 22 | private_bufMacGuid = croto.md5(f"{stru.uin}bufMacGuid".encode()) 23 | 24 | def tlv_112(ns: utils.Stream): 25 | ns.write_tlv("01 12", stru.token_0038_from_0825) 26 | packet.body.write_func(tlv_112) 27 | 28 | def tlv_30f(ns: utils.Stream): 29 | ns.write_token(b'PY-PCQQ') 30 | ns.write_tlv("03 0F", ns.read_all()) 31 | packet.body.write_func(tlv_30f) 32 | 33 | def tlv_005(ns: utils.Stream): 34 | ns.write_hex("00 02") 35 | ns.write_int32(stru.uin) 36 | 37 | ns.write_tlv("00 05", ns.read_all()) 38 | packet.body.write_func(tlv_005) 39 | 40 | if stru.is_scancode: 41 | def tlv_303(ns: utils.Stream): 42 | ns.write_tlv("03 03", stru.password) 43 | packet.body.write_func(tlv_303) 44 | 45 | private_bufOfficial = croto.create_official(bufOfficiaKey, OfficialData, stru.password[2:]) 46 | else: 47 | logger.fatal("账密登录待开发, 请优先使用扫码登录") 48 | raise RuntimeError("Can't login by password") 49 | 50 | def tlv_015(ns: utils.Stream): 51 | computer_id2 = computer_id_ex[0:4] + bytes(12) 52 | 53 | ns.write_hex("00 00 01") 54 | 55 | ns.write_int32(crc32(computer_id2)) 56 | ns.write_int16(16) 57 | ns.write(computer_id2) 58 | ns.write_byte(2) 59 | 60 | ns.write_int32(crc32(computer_id)) 61 | ns.write_int16(16) 62 | ns.write(computer_id) 63 | 64 | ns.write_tlv("00 15", ns.read_all()) 65 | packet.body.write_func(tlv_015) 66 | 67 | def tlv_01a(ns: utils.Stream): 68 | ns.write_func(tlv_015) 69 | ns.write_tlv("00 1A", croto.tea_encrypt(ns.read_all(), stru.tgt_key)) 70 | packet.body.write_func(tlv_01a) 71 | 72 | def tlv_018(ns: utils.Stream): 73 | ns.write_hex("00 01") 74 | ns.write(const.SsoVersion) 75 | ns.write(const.ServiceId) 76 | ns.write(const.ClientVersion) 77 | ns.write_int32(stru.uin) 78 | ns.write_int16(stru.redirection_times) 79 | ns.write_hex("00 00") 80 | 81 | ns.write_tlv("00 18", ns.read_all()) 82 | packet.body.write_func(tlv_018) 83 | 84 | def tlv_103(ns: utils.Stream): 85 | ns.write_hex("00 01") 86 | ns.write_hex("00 10") 87 | ns.write(private_bufMacGuid) 88 | 89 | ns.write_tlv("01 03", ns.read_all()) 90 | packet.body.write_func(tlv_103) 91 | 92 | def tlv_312(ns: utils.Stream): 93 | ns.write_tlv("03 12", bytes.fromhex("01 00 00 00 00")) 94 | packet.body.write_func(tlv_312) 95 | 96 | def tlv_508(ns: utils.Stream): 97 | ns.write_hex("01 00 00 00") 98 | ns.write_byte(2) 99 | 100 | ns.write_tlv("05 08", ns.read_all()) 101 | packet.body.write_func(tlv_508) 102 | 103 | def tlv_313(ns: utils.Stream): 104 | ns.write_hex("01 01 02") 105 | ns.write_hex("00 10") 106 | ns.write(private_bufMacGuid) 107 | ns.write_hex("00 00 00 10") 108 | 109 | ns.write_tlv("03 13", ns.read_all()) 110 | packet.body.write_func(tlv_313) 111 | 112 | def tlv_102(ns: utils.Stream): 113 | ns.write_hex("00 91") 114 | ns.write(bufOfficiaKey) 115 | 116 | ns.write_hex("00 38") 117 | ns.write(OfficialData) 118 | 119 | ns.write_hex("00 14") 120 | ns.write(private_bufOfficial) 121 | ns.write_int32(crc32(private_bufOfficial)) 122 | 123 | ns.write_tlv("01 02", ns.read_all()) 124 | packet.body.write_func(tlv_102) 125 | 126 | def tlv_501(ns: utils.Stream): 127 | vFix = bytes([67, 70, 57, 109, 89, 84, 77, 114, 109, 49, 128 | 75, 85, 48, 109, 121, 101, 54, 52, 80, 118]) 129 | v2 = 872529248 # int((time.time()-psutil.boot_time()) * 1000) 130 | 131 | v13 = bytearray() 132 | v1 = croto.rand_str2(v13) 133 | v3 = int(time()).to_bytes(8, 'little') + v1 + computer_id 134 | v4 = croto.sha1024(v3, bytes(v13)) 135 | tlv0551_key = croto.md5(v4) 136 | 137 | ns.write_int32(stru.uin) 138 | ns.write(const.SsoVersion) 139 | ns.write_int32(v2) 140 | ns.write_hex("00 00 08 36") 141 | ns.write_token(private_bufMacGuid) 142 | enc = ns.read_all() 143 | 144 | byte10 = bytearray() 145 | v23 = croto.sub_16F90(enc, byte10, vFix, v13) 146 | 147 | ns.write(const.SsoVersion) 148 | ns.write_int32(65536) 149 | ns.write_byte(109) 150 | ns.write_int32((1 | ((0 | (0 << 8)) << 8)) << 8) 151 | ns.write_int32((2 | ((0 | (0 << 8)) << 8)) << 8) 152 | ns.write_int32((2 | ((0 | (0 << 8)) << 8)) << 8) 153 | ns.write(bytes(byte10)) 154 | ns.write_int32(v2) 155 | ns.write_int32(4) 156 | ns.write_byte(20) 157 | ns.write(v23) 158 | ns.write(tlv0551_key) 159 | ns.write_token(b'5151') 160 | 161 | ns.write_tlv("05 51", ns.read_all()) 162 | packet.body.write_func(tlv_501) 163 | 164 | return packet -------------------------------------------------------------------------------- /core/protocol/wtlogin/p4_0836_login/unpack.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from core import utils, croto 4 | from logger import logger 5 | from core import const 6 | from core.entities import QQStruct, Packet, PacketHandler 7 | 8 | 9 | def check_0836(packet: Packet): 10 | return packet.cmd == "08 36" 11 | 12 | 13 | async def handle_0836(stru: QQStruct, packet: Packet): 14 | if packet.body.read_hex(2) != "01 03": 15 | raise ValueError("08 36 Response Protocol Packet error") 16 | 17 | tk_key = packet.body.read_token() 18 | twice_key = stru.ecdh.twice(tk_key) 19 | raw_data = croto.tea_decrypt(packet.body.read_all(), twice_key) 20 | 21 | raw_data = croto.tea_decrypt(raw_data, stru.tgt_key) 22 | if raw_data == b'': 23 | raw_data = croto.tea_decrypt(raw_data, const.RandKey) 24 | stream = utils.Stream(raw_data) 25 | 26 | sign = stream.read_byte() 27 | if sign == 0x00: 28 | # logger.info(f"登录状态校验 -> OK") 29 | pass 30 | elif sign == 0x01: 31 | logger.fatal("登录状态校验 -> 需要更新TGTGT或需要二次解密") 32 | exit(1) 33 | elif sign == 0x33: 34 | logger.fatal("登录状态校验 -> 当前上网环境异常") 35 | exit(1) 36 | elif sign == 0x34: 37 | logger.fatal("登录状态校验 -> 需要验证密保或开启了设备锁") 38 | exit(1) 39 | else: 40 | logger.fatal("登录状态校验 -> 发生未处理错误") 41 | exit(1) 42 | 43 | while len(stream._raw) > 0: 44 | cmd = stream.read_hex(2) 45 | ns = utils.Stream(stream.read_token()) 46 | 47 | if cmd == "01 09": 48 | stru.pckey_for_0828_send = ns.del_left(2).read(16) 49 | stru.token_0038_from_0836 = ns.read_token() 50 | 51 | elif cmd == "01 07": 52 | ns.read_int16() 53 | ns.read_token() 54 | 55 | stru.pckey_for_0828_recv = ns.read(16) 56 | stru.token_0088_from_0836 = ns.read_token() 57 | 58 | elif cmd == "01 08": 59 | ns.read(8) 60 | length = ns.read_byte() 61 | stru.nickname = ns.read(length).decode() 62 | 63 | 64 | def unpack_0836(stru: QQStruct): 65 | return PacketHandler( 66 | temp=True, 67 | check=check_0836, 68 | handle=partial(handle_0836, stru) 69 | ) 70 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/p5_0828_session/__init__.py: -------------------------------------------------------------------------------- 1 | from .pack import pack_0828 2 | from .unpack import unpack_0828 -------------------------------------------------------------------------------- /core/protocol/wtlogin/p5_0828_session/pack.py: -------------------------------------------------------------------------------- 1 | from binascii import crc32 2 | 3 | from core import utils, croto 4 | from core import const 5 | from core.entities import Packet, QQStruct 6 | 7 | 8 | def pack_0828(stru: QQStruct): 9 | packet = Packet( 10 | cmd="08 28", 11 | uin=stru.uin, 12 | tea_key=stru.pckey_for_0828_send 13 | ) 14 | packet.version.write(const.BodyVersion).write_hex("00 30 00 3A 00 38") 15 | packet.version.write(stru.token_0038_from_0836) 16 | 17 | def tlv_007(ns: utils.Stream): 18 | ns.write_tlv("00 07", stru.token_0088_from_0836) 19 | packet.body.write_func(tlv_007) 20 | 21 | def tlv_00c(ns: utils.Stream): 22 | ns.write_hex("00 02 00 00 00 00 00 00 00 00 00 00") 23 | ns.write(stru.server_ip) 24 | ns.write_int16(stru.addr[1]) 25 | ns.write_hex("00 00 00 00") 26 | 27 | ns.write_tlv("00 0C", ns.read_all()) 28 | packet.body.write_func(tlv_00c) 29 | 30 | def tlv_015(ns: utils.Stream): 31 | computer_id = croto.md5(f"{stru.uin}ComputerID".encode()) 32 | computer_id2 = croto.md5(f"{stru.uin}ComputerIDEx".encode()) 33 | 34 | ns.write_hex("00 00 01") 35 | 36 | ns.write_int32(crc32(computer_id2)) 37 | ns.write_int16(16) 38 | ns.write(computer_id2) 39 | ns.write_byte(2) 40 | 41 | ns.write_int32(crc32(computer_id)) 42 | ns.write_int16(16) 43 | ns.write(computer_id) 44 | 45 | ns.write_tlv("00 15", ns.read_all()) 46 | packet.body.write_func(tlv_015) 47 | 48 | def tlv_036(ns: utils.Stream): 49 | ns.write_hex("00 02") 50 | ns.write_hex("00 01") 51 | ns.write(bytes(14)) 52 | 53 | ns.write_tlv("00 36", ns.read_all()) 54 | packet.body.write_func(tlv_036) 55 | 56 | def tlv_018(ns: utils.Stream): 57 | ns.write_hex("00 01") 58 | ns.write(const.SsoVersion) 59 | ns.write(const.ServiceId) 60 | ns.write(const.ClientVersion) 61 | ns.write_int32(stru.uin) 62 | ns.write_int16(stru.redirection_times) 63 | ns.write_hex("00 00") 64 | 65 | ns.write_tlv("00 18", ns.read_all()) 66 | packet.body.write_func(tlv_018) 67 | 68 | def tlv_01f(ns: utils.Stream): 69 | ns.write_hex("00 01") 70 | ns.write(const.DeviceID) 71 | 72 | ns.write_tlv("00 1F", ns.read_all()) 73 | packet.body.write_func(tlv_01f) 74 | 75 | def tlv_105(ns: utils.Stream): 76 | ns.write_int16(1) 77 | ns.write_hex("01 02") 78 | 79 | ns.write_int16(20) 80 | ns.write_hex("01 01") 81 | ns.write_int16(16) 82 | ns.write(utils.rand_bytes(16)) 83 | 84 | ns.write_int16(20) 85 | ns.write_hex("01 02") 86 | ns.write_int16(16) 87 | ns.write(utils.rand_bytes(16)) 88 | 89 | ns.write_tlv("01 05", ns.read_all()) 90 | packet.body.write_func(tlv_105) 91 | 92 | def tlv_10b(ns: utils.Stream): 93 | ns.write_hex("00 02") 94 | ns.write(utils.rand_bytes(16)) # ClientMD5 服务端不作校验 95 | ns.write(utils.rand_bytes(1)) # QDFlag 服务端不作校验 96 | ns.write_hex("10 00 00 00 00 00 00 00 02") 97 | 98 | # QDDATA 服务端不作校验 99 | 100 | ns.write_hex("00 63") 101 | ns.write_hex("3E 00 63 02") 102 | ns.write(const.DWQDVersion) 103 | ns.write_hex("00 04 00") 104 | ns.write(utils.rand_bytes(2)) 105 | ns.write_hex("00 00 00 00") 106 | ns.write(utils.rand_bytes(16)) 107 | 108 | ns.write_hex("01 00") 109 | ns.write_hex("00 00 00 00") 110 | ns.write_hex("00 00") 111 | ns.write_hex("00 00 00 00 00 00 00 00") 112 | 113 | ns.write_hex("00 00 00 01") 114 | ns.write_hex("00 00 00 01") 115 | ns.write_hex("00 00 00 01") 116 | ns.write_hex("00 00 00 01") 117 | ns.write_hex("00 FE 26 81 75 EC 2A 34 EF") 118 | 119 | ns.write_hex("02 3E 50 39 6D B1 AF CC 9F EA 54 E1") 120 | ns.write_hex("70 CC 6C 9E 4E 63 8B 51 EC 7C 84 5C") 121 | 122 | ns.write_hex("68") 123 | ns.write_hex("00 00 00 00") 124 | 125 | ns.write_tlv("01 0B", ns.read_all()) 126 | packet.body.write_func(tlv_10b) 127 | 128 | def tlv_20d(ns: utils.Stream): 129 | ns.write_hex("00 01") 130 | ns.write(stru.local_ip) 131 | 132 | ns.write_tlv("02 0D", ns.read_all()) 133 | packet.body.write_func(tlv_20d) 134 | 135 | return packet 136 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/p5_0828_session/unpack.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from logger import logger 4 | from core.utils import Stream 5 | from core.entities import QQStruct, Packet, PacketHandler 6 | 7 | 8 | def check_0828(packet: Packet): 9 | return packet.cmd == "08 28" 10 | 11 | 12 | async def handle_0828(stru: QQStruct, packet: Packet): 13 | sign = packet.body.read_byte() 14 | if sign == 0x00: 15 | #logger.info("会话密匙申请 -> OK") 16 | pass 17 | else: 18 | logger.fatal("会话密匙申请 -> 发生未处理错误") 19 | exit(1) 20 | 21 | while len(packet.body._raw) > 0: 22 | cmd = packet.body.read_hex(2) 23 | ns = Stream(packet.body.read_token()) 24 | 25 | if cmd == "01 0C": 26 | stru.session_key = ns.del_left(2).read(16) 27 | 28 | 29 | def unpack_0828(stru: QQStruct): 30 | return PacketHandler( 31 | temp=True, 32 | check=check_0828, 33 | handle=partial(handle_0828, stru) 34 | ) 35 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/p6_00ec_online/__init__.py: -------------------------------------------------------------------------------- 1 | from .pack import pack_00ec 2 | from .unpack import unpack_00ec -------------------------------------------------------------------------------- /core/protocol/wtlogin/p6_00ec_online/pack.py: -------------------------------------------------------------------------------- 1 | from core import const 2 | from core.entities import Packet, QQStruct 3 | 4 | 5 | def pack_00ec(stru: QQStruct): 6 | packet = Packet( 7 | cmd="00 EC", 8 | uin=stru.uin, 9 | tea_key=stru.session_key 10 | ) 11 | packet.version.write(const.BodyVersion) 12 | 13 | packet.body.write_hex("01 00") 14 | packet.body.write_byte(const.StateOnline) # 上线 15 | packet.body.write_hex("00 01") 16 | packet.body.write_hex("00 01") 17 | packet.body.write_hex("00 04") 18 | packet.body.write_hex("00 00 00 00") 19 | 20 | return packet -------------------------------------------------------------------------------- /core/protocol/wtlogin/p6_00ec_online/unpack.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from logger import logger 4 | from core.entities import Packet, PacketHandler, QQStruct 5 | 6 | 7 | def check_00ec(packet: Packet): 8 | return packet.cmd == "00 EC" 9 | 10 | 11 | async def handle_00ec(stru: QQStruct, packet: Packet): 12 | stru.is_running = True 13 | 14 | if stru.password != b'': 15 | logger.info(f"扫码登录成功, 欢迎尊敬的用户 {stru.nickname}({stru.uin}) 使用本协议库") 16 | else: 17 | logger.info(f"Token重登成功, 欢迎尊敬的用户 {stru.nickname}({stru.uin}) 使用本协议库") 18 | 19 | 20 | def unpack_00ec(stru: QQStruct): 21 | return PacketHandler( 22 | temp=True, 23 | check=check_00ec, 24 | handle=partial(handle_00ec, stru) 25 | ) 26 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/p7_001d_skey/__init__.py: -------------------------------------------------------------------------------- 1 | from .pack import pack_001d 2 | from .unpack import unpack_001d -------------------------------------------------------------------------------- /core/protocol/wtlogin/p7_001d_skey/pack.py: -------------------------------------------------------------------------------- 1 | from core import const 2 | from core.entities import Packet, QQStruct 3 | 4 | 5 | def pack_001d(stru: QQStruct): 6 | packet = Packet( 7 | cmd="00 1D", 8 | uin=stru.uin, 9 | tea_key=stru.session_key 10 | ) 11 | packet.version.write(const.BodyVersion) 12 | 13 | packet.body.write_byte(51) 14 | packet.body.write_int16(6) # 域名数量 15 | packet.body.write_token(b't.qq.com') 16 | packet.body.write_token(b'qun.qq.com') 17 | packet.body.write_token(b'qzone.qq.com') 18 | packet.body.write_token(b'jubao.qq.com') 19 | packet.body.write_token(b'ke.qq.com') 20 | packet.body.write_token(b'tenpay.com') 21 | 22 | return packet -------------------------------------------------------------------------------- /core/protocol/wtlogin/p7_001d_skey/unpack.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from core.entities import Packet, PacketHandler, QQStruct 3 | 4 | 5 | def check_001d(packet: Packet): 6 | return packet.cmd == "00 1D" 7 | 8 | 9 | async def handle_001d(stru: QQStruct, packet: Packet): 10 | packet.body.read(2) 11 | stru.skey = packet.body.read_token().decode() 12 | stru.bkn = gtk_skey(stru.skey) 13 | stru.cookie = "uin=o%d; skey=%s" % (stru.uin, stru.skey) 14 | 15 | 16 | def unpack_001d(stru: QQStruct): 17 | return PacketHandler( 18 | temp=True, 19 | check=check_001d, 20 | handle=partial(handle_001d, stru) 21 | ) 22 | 23 | 24 | def gtk_skey(skey: str) -> int: 25 | accu = 5381 26 | for s in skey: 27 | accu += (accu << 5) + ord(s) 28 | return accu & 2147483647 29 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/p8_0058_heartbeat/__init__.py: -------------------------------------------------------------------------------- 1 | from .pack import pack_0058 2 | from .unpack import unpack_0058 -------------------------------------------------------------------------------- /core/protocol/wtlogin/p8_0058_heartbeat/pack.py: -------------------------------------------------------------------------------- 1 | from core import const 2 | from core.entities import Packet, QQStruct 3 | 4 | 5 | def pack_0058(stru: QQStruct): 6 | packet = Packet( 7 | cmd="00 58", 8 | uin=stru.uin, 9 | tea_key=stru.session_key 10 | ) 11 | 12 | packet.version.write(const.BodyVersion) 13 | packet.body.write_hex("00 01 00 01") 14 | 15 | return packet 16 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/p8_0058_heartbeat/unpack.py: -------------------------------------------------------------------------------- 1 | from logger import logger 2 | from core.entities import Packet, PacketHandler, QQStruct 3 | 4 | 5 | def check_0058(packet: Packet): 6 | return packet.cmd == "00 58" 7 | 8 | 9 | async def handle_0058(packet: Packet): 10 | sign = packet.body.read_byte() 11 | 12 | if sign == 0x00: 13 | # logger.info("心跳正常") 14 | pass 15 | elif sign == 0x01: 16 | logger.error("当前客户端掉线, 请刷新在线状态") 17 | elif sign == 0x17: 18 | logger.error("当前客户端掉线, 请重新登录") 19 | 20 | 21 | def unpack_0058(stru: QQStruct): 22 | return PacketHandler( 23 | temp=False, 24 | check=check_0058, 25 | handle=handle_0058 26 | ) 27 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/p9_0062_login_out/__init__.py: -------------------------------------------------------------------------------- 1 | from .pack import pack_0062 -------------------------------------------------------------------------------- /core/protocol/wtlogin/p9_0062_login_out/pack.py: -------------------------------------------------------------------------------- 1 | from core import const 2 | from core.entities import Packet, QQStruct 3 | 4 | 5 | def pack_0062(stru: QQStruct): 6 | packet = Packet( 7 | cmd="00 62", 8 | uin=stru.uin, 9 | tea_key=stru.session_key 10 | ) 11 | packet.version.write(const.BodyVersion) 12 | 13 | packet.body.write(bytes(16)) 14 | stru.is_running = False 15 | return packet 16 | -------------------------------------------------------------------------------- /core/protocol/wtlogin/wtlogin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import asyncio 4 | 5 | from logger import logger 6 | from core.utils import Stream 7 | from core import const 8 | from core.client import QQClient 9 | 10 | from .p1_0825_ping import pack_0825, unpack_0825 11 | from .p2_0818_qrcode import pack_0818, unpack_0818 12 | from .p3_0819_scan import pack_0819, unpack_0819 13 | from .p4_0836_login import pack_0836, unpack_0836 14 | from .p5_0828_session import pack_0828, unpack_0828 15 | from .p6_00ec_online import pack_00ec, unpack_00ec 16 | from .p7_001d_skey import pack_001d, unpack_001d 17 | from .p8_0058_heartbeat import pack_0058, unpack_0058 18 | from .p9_0062_login_out import pack_0062 19 | 20 | def login_out(cli: QQClient): 21 | if not cli.stru.is_running: 22 | raise RuntimeError("当前客户端还未登录, 无法执行下线操作") 23 | 24 | cli.send(pack_0062(cli.stru)) 25 | logger.info("已退出当前登录, 期待您的下次使用") 26 | 27 | 28 | async def login_by_scan(cli: QQClient): 29 | # ping 服务器 30 | cli.send(pack_0825(cli.stru)) 31 | cli.manage.append(unpack_0825(cli.stru)) 32 | await cli.recv_and_exec(const.RandKey) 33 | 34 | # 申请登录二维码 35 | cli.send(pack_0818(cli.stru)) 36 | cli.manage.append(unpack_0818(cli.stru)) 37 | await cli.recv_and_exec(cli.stru.ecdh.share_key) 38 | 39 | # 每两秒判断一次扫码结果 40 | while cli.stru.password == b'': 41 | await asyncio.sleep(2) 42 | 43 | cli.send(pack_0819(cli.stru)) 44 | cli.manage.append(unpack_0819(cli.stru)) 45 | await cli.recv_and_exec(cli.stru.pckey_for_0819) 46 | 47 | # 进行登录校验 48 | cli.send(pack_0836(cli.stru)) 49 | cli.manage.append(unpack_0836(cli.stru)) 50 | await cli.recv_and_exec(cli.stru.ecdh.share_key) 51 | 52 | # 申请会话密匙 53 | cli.send(pack_0828(cli.stru)) 54 | cli.manage.append(unpack_0828(cli.stru)) 55 | await cli.recv_and_exec(cli.stru.pckey_for_0828_recv) 56 | 57 | # 申请网络密匙 58 | cli.send(pack_001d(cli.stru)) 59 | cli.manage.append(unpack_001d(cli.stru)) 60 | await cli.recv_and_exec(cli.stru.session_key) 61 | 62 | # 置上线状态 63 | cli.send(pack_00ec(cli.stru)) 64 | cli.manage.append(unpack_00ec(cli.stru)) 65 | await cli.recv_and_exec(cli.stru.session_key) 66 | 67 | # 开启心跳循环 68 | async def heart_beat(): 69 | cli.send(pack_0058(cli.stru)) 70 | await asyncio.sleep(40) 71 | cli.add_task(heart_beat()) 72 | cli.manage.append(unpack_0058(cli.stru)) 73 | cli.add_task(heart_beat()) 74 | 75 | # 写入 Token 用于二次登录 76 | path = os.path.join(cli.stru.path, "session.token") 77 | with open(path, "wb") as f: 78 | stream = Stream() 79 | 80 | stream.write_token(cli.stru.server_ip) 81 | stream.write_int32(cli.stru.uin) 82 | stream.write_token(cli.stru.nickname.encode()) 83 | 84 | stream.write_token(cli.stru.token_0038_from_0836) 85 | stream.write_token(cli.stru.token_0088_from_0836) 86 | stream.write_token(cli.stru.pckey_for_0828_send) 87 | stream.write_token(cli.stru.pckey_for_0828_recv) 88 | f.write(stream.read_all()) 89 | 90 | 91 | logger.info(f"已写入 Token 至 {path}, 下次登录无需再进行扫码") 92 | 93 | async def login_by_token(cli: QQClient): 94 | # ping 服务器 95 | cli.send(pack_0825(cli.stru)) 96 | cli.manage.append(unpack_0825(cli.stru)) 97 | await cli.recv_and_exec(const.RandKey) 98 | 99 | # 读取 Token 用于二次登录 100 | path = os.path.join(cli.stru.path, "session.token") 101 | if not os.path.exists(path): 102 | logger.error(f"{path} 所指向的 Token 文件不存在, 请先使用扫码登录生成") 103 | sys.exit(0) 104 | 105 | with open(path, "rb") as f: 106 | stream = Stream(f.read()) 107 | 108 | cli.stru.server_ip = stream.read_token() 109 | cli.stru.uin = stream.read_int32() 110 | cli.stru.nickname = stream.read_token().decode() 111 | cli.stru.token_0038_from_0836 = stream.read_token() 112 | cli.stru.token_0088_from_0836 = stream.read_token() 113 | cli.stru.pckey_for_0828_send = stream.read_token() 114 | cli.stru.pckey_for_0828_recv = stream.read_token() 115 | 116 | try: 117 | # 申请会话密匙 118 | cli.send(pack_0828(cli.stru)) 119 | cli.manage.append(unpack_0828(cli.stru)) 120 | 121 | coro = cli.recv_and_exec(cli.stru.pckey_for_0828_recv) 122 | await asyncio.wait_for(coro, timeout=10) 123 | except: 124 | os.remove(path) 125 | logger.error(f"本地Token已失效, 请重新运行程序使用扫码登录") 126 | sys.exit(0) 127 | 128 | # 置上线状态 129 | cli.send(pack_001d(cli.stru)) 130 | cli.manage.append(unpack_001d(cli.stru)) 131 | await cli.recv_and_exec(cli.stru.session_key) 132 | 133 | # 置上线状态 134 | cli.send(pack_00ec(cli.stru)) 135 | cli.manage.append(unpack_00ec(cli.stru)) 136 | await cli.recv_and_exec(cli.stru.session_key) 137 | 138 | # 开启心跳循环 139 | async def heart_beat(): 140 | cli.send(pack_0058(cli.stru)) 141 | await asyncio.sleep(40) 142 | cli.add_task(heart_beat()) 143 | cli.manage.append(unpack_0058(cli.stru)) 144 | cli.add_task(heart_beat()) -------------------------------------------------------------------------------- /core/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .rand import ( 2 | rand_bytes, 3 | rand_tcp_host, 4 | rand_udp_host 5 | ) 6 | 7 | from .qrcode import QRCode 8 | 9 | from .stream import Stream 10 | 11 | from .socket import AsyncTCPSocket, UDPSocket 12 | -------------------------------------------------------------------------------- /core/utils/qrcode.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import subprocess 4 | 5 | 6 | class QRCode: 7 | def __init__(self, path: str, data: bytes, qr_len=37) -> None: 8 | self.path = os.path.join(path, "QrCode.jpg") 9 | with open(self.path, mode="wb") as f: 10 | f.write(data) 11 | 12 | self.qr_len = qr_len 13 | 14 | def open_in_windows(self): 15 | return os.startfile(self.path) 16 | 17 | def open_in_linux(self): 18 | return subprocess.call(["xdg-open", self.path]) 19 | 20 | def open_in_macos(self): 21 | return subprocess.call(["open", self.path]) 22 | 23 | def print_in_terminal(self): 24 | from PIL import Image 25 | 26 | im = Image.open(self.path) 27 | im = im.resize((self.qr_len, self.qr_len), Image.NEAREST) 28 | 29 | txt = '' 30 | for i in range(self.qr_len): 31 | for j in range(self.qr_len): 32 | content = im.getpixel((j, i)) 33 | if isinstance(content, int): 34 | content = (content, content, content) 35 | txt += rgb_to_char(*content) 36 | txt += '\n' 37 | im.close() 38 | print(make_qrtex(txt)) 39 | 40 | def auto_show(self): 41 | system = platform.system() 42 | try: 43 | # 用系统默认程序打开图片文件 44 | if system == "Windows": 45 | self.open_in_windows() 46 | elif system == "Linux": 47 | self.open_in_linux() 48 | elif system == "Darwin": 49 | self.open_in_macos() 50 | else: 51 | raise SystemError("This device system is unknown") 52 | except: 53 | # 使用pillow库在终端上打印二维码图片 54 | self.print_in_terminal() 55 | 56 | 57 | def rgb_to_char(r, g, b, alpha=256): 58 | if alpha == 0: 59 | return ' ' 60 | gary = (2126 * r + 7152 * g + 722 * b) / 10000 61 | ascii_char = list("■□") 62 | # gary / 256 = x / len(ascii_char) 63 | x = int(gary / (alpha + 1.0) * len(ascii_char)) 64 | return ascii_char[x] 65 | 66 | 67 | def make_qrtex(QR_Tab): 68 | tmp_text = "" 69 | print_tex = "" 70 | 71 | atrr = 7 72 | fore = 37 73 | back = 47 74 | color_block = "\x1B[%d;%d;%dm" % (atrr, fore, back) 75 | atrr = 0 76 | fore = 0 77 | back = 0 78 | color_none = "\x1B[%d;%d;%dm" % (atrr, fore, back) 79 | 80 | for loop in range(0, len(QR_Tab)): 81 | if QR_Tab[loop] == '■': 82 | tmp_text = "%s \x1B[0m" % (color_block) 83 | elif QR_Tab[loop] == '□': 84 | tmp_text = "%s \x1B[0m" % (color_none) 85 | else: 86 | tmp_text = "\n" 87 | 88 | print_tex = print_tex + tmp_text 89 | 90 | return print_tex 91 | -------------------------------------------------------------------------------- /core/utils/rand.py: -------------------------------------------------------------------------------- 1 | import random 2 | import socket 3 | 4 | 5 | def rand_bytes(size: int): 6 | return bytes([random.randint(0, 255) for _ in range(size)]) 7 | 8 | 9 | def rand_tcp_host(): 10 | host = random.choice([ 11 | "tcpconn.tencent.com", 12 | "tcpconn2.tencent.com", 13 | "tcpconn3.tencent.com", 14 | "tcpconn4.tencent.com" 15 | ]) 16 | 17 | return socket.gethostbyname(host) 18 | 19 | 20 | def rand_udp_host(): 21 | host = random.choice([ 22 | "sz.tencent.com", 23 | "sz2.tencent.com", 24 | "sz3.tencent.com", 25 | "sz4.tencent.com", 26 | "sz5.tencent.com", 27 | "sz6.tencent.com", 28 | "sz7.tencent.com", 29 | "sz8.tencent.com", 30 | "sz9.tencent.com", 31 | ]) 32 | 33 | return socket.gethostbyname(host) -------------------------------------------------------------------------------- /core/utils/socket.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import asyncio 3 | 4 | 5 | class AsyncTCPSocket: 6 | def __init__(self, host, port, loop): 7 | self.host = host 8 | self.port = port 9 | self.loop = loop 10 | self.lock = asyncio.Lock(loop=loop) 11 | 12 | async def init(self): 13 | reader, writer = await asyncio.open_connection( 14 | host=self.host, 15 | port=self.port, 16 | loop=self.loop 17 | ) 18 | 19 | self.reader = reader 20 | self.writer = writer 21 | 22 | def send(self, data: bytes): 23 | size = len(data) + 2 24 | self.writer.write(size.to_bytes(2, "big") + data) 25 | 26 | async def recv(self): 27 | async with self.lock: 28 | temp = await self.reader.read(2) 29 | size = int.from_bytes(temp, "big") 30 | 31 | data = await self.reader.read(size) 32 | return data 33 | 34 | 35 | class UDPSocket: 36 | def __init__(self, host: str, port: int): 37 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 38 | self.sock.settimeout(60) 39 | 40 | self.host = socket.gethostbyname(host) 41 | self.port = port 42 | 43 | def send(self, data: bytes): 44 | return self.sock.sendto(data, (self.host, self.port)) 45 | 46 | def recv(self): 47 | data, _ = self.sock.recvfrom(1024 * 10) 48 | return data 49 | -------------------------------------------------------------------------------- /core/utils/stream.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class Stream: 5 | def __init__(self, raw=bytes(0)): 6 | self._raw = raw 7 | 8 | def __str__(self): 9 | if self._raw == None: 10 | self._raw = b'' 11 | 12 | str = re.findall(".{2}", self._raw.hex().upper()) 13 | return f"Stream({' '.join(str)})" 14 | 15 | def __repr__(self): 16 | if self._raw == None: 17 | self._raw = b'' 18 | return f"Stream(Size: {len(self._raw)})" 19 | 20 | def write(self, bin: bytes): 21 | self._raw += bin 22 | return self 23 | 24 | def write_int16(self, num: int): 25 | return self.write(num.to_bytes(2, 'big')) 26 | 27 | def write_int32(self, num: int): 28 | return self.write(num.to_bytes(4, 'big')) 29 | 30 | def write_varint(self, num: int): 31 | val = bytearray() 32 | while num > 127: 33 | val.append(0x80 | num & 0x7F) 34 | num = num >> 7 35 | val.append(num) 36 | return self.write(bytes(val)) 37 | 38 | def write_pbint(self, num: int, mark: int): 39 | mark = 8 * mark 40 | self.write_varint(mark) 41 | self.write_varint(num) 42 | 43 | def write_pbdata(self, data: bytes, mark: int): 44 | mark = 2 + 8 * mark 45 | self.write_varint(mark) 46 | self.write_byte(len(data)) 47 | self.write(data) 48 | 49 | def write_pbhex(self, hex: str, mark: int): 50 | return self.write_pbdata(bytes.fromhex(hex), mark) 51 | 52 | def write_byte(self, byte: int): 53 | return self.write(byte.to_bytes(1, 'big')) 54 | 55 | def write_list(self, num_list: list): 56 | return self.write(bytes(num_list)) 57 | 58 | def write_hex(self, hex: str): 59 | return self.write(bytes.fromhex(hex)) 60 | 61 | def write_token(self, token: bytes): 62 | return self.write_int16(len(token)).write(token) 63 | 64 | def write_tlv(self, cmd: str, bin: bytes): 65 | return self.write_hex(cmd).write_token(bin) 66 | 67 | def write_stream(self, new_stream): 68 | return self.write(new_stream.read_all()) 69 | 70 | def write_func(self, func): 71 | new_stream = Stream(b'') 72 | func(new_stream) 73 | return self.write_stream(new_stream) 74 | 75 | def del_left(self, length: int): 76 | self._raw = self._raw[length:] 77 | return self 78 | 79 | def del_right(self, length: int): 80 | self._raw = self._raw[:-length] 81 | return self 82 | 83 | def read(self, size: int): 84 | bin = self._raw[0:size] 85 | self._raw = self._raw[size:] 86 | return bin 87 | 88 | def read_all(self): 89 | return self.read(len(self._raw)) 90 | 91 | def read_byte(self): 92 | return self.read(1)[0] 93 | 94 | def read_int16(self): 95 | return int.from_bytes(self.read(2), 'big') 96 | 97 | def read_int32(self): 98 | return int.from_bytes(self.read(4), 'big') 99 | 100 | def read_hex(self, size: int): 101 | string = self.read(size).hex().upper() 102 | return " ".join(re.findall(".{2}", string)) 103 | 104 | def read_token(self, keep_size=False): 105 | size = self.read_int16() 106 | data = self.read(size) 107 | return size.to_bytes(2, 'big') + data if keep_size else data -------------------------------------------------------------------------------- /default/__init__.py: -------------------------------------------------------------------------------- 1 | from .bot import run_bot 2 | from .plugin import ( 3 | Session, 4 | only_group, 5 | only_private, 6 | check_user, 7 | check_session, 8 | on_event, 9 | on_command, 10 | on_message 11 | ) -------------------------------------------------------------------------------- /default/bot.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from core.client import QQClient 4 | from core.entities import MessageEvent 5 | from core.protocol import wtlogin, event 6 | from .plugin import Session, default_manager 7 | 8 | 9 | def run_bot(uin: int = 0): 10 | cli = QQClient(uin) 11 | 12 | path = os.path.join(cli.stru.path, "session.token") 13 | if os.path.exists(path): 14 | cli.run_task(wtlogin.login_by_token(cli)) 15 | else: 16 | cli.run_task(wtlogin.login_by_scan(cli)) 17 | 18 | async def plugin_handle_message(event: MessageEvent): 19 | session = Session( 20 | driver=cli, 21 | message=event.message.format(), 22 | event=event, 23 | matched=None 24 | ) 25 | 26 | await default_manager.exec_all(session) 27 | 28 | cli.manage.append(event.handle_msg_receipt(cli)) 29 | cli.manage.append(event.handle_group_msg(cli, plugin_handle_message)) 30 | cli.manage.append(event.handle_private_msg(cli, plugin_handle_message)) 31 | 32 | async def run_handler(): 33 | await cli.recv_and_exec(cli.stru.session_key) 34 | cli.add_task(run_handler()) 35 | cli.add_task(run_handler()) 36 | 37 | cli.loop.run_forever() -------------------------------------------------------------------------------- /default/plugin.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | from asyncio import Queue 4 | from dataclasses import dataclass 5 | from typing import Any, List, Union, Iterable, Callable, Coroutine 6 | 7 | from logger import logger 8 | from core.protocol import pcapi, webapi 9 | from core.client import QQClient 10 | from core.entities import ( 11 | MessageEvent, 12 | NoticeEvent, 13 | Message, 14 | TextNode 15 | ) 16 | 17 | 18 | @dataclass 19 | class Session: 20 | driver: QQClient 21 | message: str 22 | event: Union[MessageEvent, NoticeEvent] 23 | matched: Union[str, list] 24 | 25 | async def aget(self, prompt: str): 26 | await self.send_msg(prompt) 27 | chan = Queue(maxsize=1, loop=self.driver.loop) 28 | 29 | @on_event(check_session(self), temp=True, block=True) 30 | async def wait_input(session: Session): 31 | await chan.put(session.message) 32 | 33 | return await chan.get() 34 | 35 | async def send_msg(self, text: str): 36 | if self.event.group_id != 0: 37 | await self.send_group_msg(self.event.group_id, text) 38 | elif self.event.user_id != 0: 39 | await self.send_private_msg(self.event.user_id, text) 40 | 41 | async def send_group_msg(self, group_id: int, text: str): 42 | node = TextNode(text) 43 | pcapi.send_group_msg(self.driver, group_id, Message().add(node)) 44 | 45 | group = await webapi.get_group(self.driver, group_id) 46 | logger.info(f"发送群聊 {group.name}({group_id}) 消息 -> {text}") 47 | 48 | async def send_private_msg(self, user_id: int, text: str): 49 | node = TextNode(text) 50 | pcapi.send_private_msg(self.driver, user_id, Message().add(node)) 51 | 52 | user = await webapi.get_user(self.driver, user_id) 53 | logger.info(f"发送私聊 {user.name}({user_id}) 消息 -> {text}") 54 | 55 | async def set_group_card(self, group_id: int, user_id: int, new_card: str): 56 | await webapi.set_group_card( 57 | self.driver.stru, 58 | group_id, 59 | user_id, 60 | new_card 61 | ) 62 | 63 | 64 | Rule = Callable[[Session], Coroutine[Any, Any, bool]] 65 | 66 | 67 | @dataclass 68 | class Plugin: 69 | rules: Iterable[Rule] 70 | temp: bool 71 | block: bool 72 | priority: int 73 | handle: Callable[[Session], Coroutine[Any, Any, None]] 74 | 75 | async def exec(self, session: Session): 76 | for rule in self.rules: 77 | if await rule(session): 78 | continue 79 | return None 80 | await self.handle(session) 81 | 82 | 83 | class PluginManager(List[Plugin]): 84 | def append(self, plugin: Plugin): 85 | if not isinstance(plugin, Plugin): 86 | raise ValueError("PluginManager 添加的元素不是 Plugin 类型") 87 | 88 | if not plugin in self: 89 | super().append(plugin) 90 | 91 | return self 92 | 93 | def pop(self, plugin: Plugin): 94 | if plugin in self: 95 | super().pop(self.index(plugin)) 96 | 97 | return self 98 | 99 | async def exec_all(self, session: Session): 100 | try: 101 | for plugin in self: 102 | await plugin.exec(session) 103 | if plugin.temp: 104 | self.pop(plugin) 105 | if plugin.block: 106 | continue 107 | except: 108 | traceback.print_exc() 109 | 110 | # =============== Buttlin Rule =============== 111 | 112 | 113 | async def only_message(session: Session): 114 | return isinstance(session.event, MessageEvent) 115 | 116 | 117 | async def only_notice(session: Session): 118 | return isinstance(session.event, NoticeEvent) 119 | 120 | 121 | async def only_private(session: Session): 122 | event = session.event 123 | return event.group_id == 0 and event.user_id != 0 124 | 125 | 126 | async def only_group(session: Session): 127 | return session.event.group_id != 0 128 | 129 | 130 | def check_user(*user_ids: int): 131 | async def new_rule(session: Session): 132 | return session.event.user_id in user_ids 133 | return new_rule 134 | 135 | 136 | def check_session(session: Session): 137 | async def new_rule(next_session: Session): 138 | event = session.event 139 | next_event = next_session.event 140 | 141 | if event.user_id != next_event.user_id: 142 | return False 143 | 144 | if event.group_id != next_event.group_id: 145 | return False 146 | 147 | return True 148 | 149 | return new_rule 150 | 151 | # =============== Buttlin Plugin Registrar =============== 152 | 153 | 154 | default_manager = PluginManager() 155 | 156 | 157 | def on_event(*rules: Rule, temp=False, block=False, priority=10): 158 | def decorator(handle: Callable[[Session], Coroutine[Any, Any, None]]): 159 | default_manager.append(Plugin( 160 | rules=rules, 161 | temp=temp, 162 | block=block, 163 | priority=priority, 164 | handle=handle 165 | )) 166 | 167 | return decorator 168 | 169 | 170 | def on_message(*rules: Rule, temp=False, block=False, priority=10): 171 | return on_event( 172 | only_message, 173 | *rules, 174 | temp=temp, 175 | block=block, 176 | priority=priority 177 | ) 178 | 179 | 180 | def on_command(cmd: str, *rules: Rule, temp=False, block=False, priority=10): 181 | async def cmd_rule(session: Session): 182 | text = session.message 183 | if text.startswith(cmd): 184 | session.matched = text.replace(cmd, "", 1).strip() 185 | return True 186 | return False 187 | 188 | return on_event( 189 | cmd_rule, 190 | *rules, 191 | temp=temp, 192 | block=block, 193 | priority=priority 194 | ) 195 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Wed Mar 2 at 22:14 2023 3 | 4 | 联系方式: 5 | @QQ: 2224825532 6 | @Github: https://github.com/DawnNights 7 | 8 | """ 9 | 10 | import default as pcqq 11 | 12 | @pcqq.on_message() 13 | async def speak(session: pcqq.Session): 14 | if session.message == "复读": 15 | sentence = await session.aget("请输入要复读的话") 16 | await session.send_msg(sentence) 17 | 18 | pcqq.run_bot() 19 | dict.setdefault -------------------------------------------------------------------------------- /logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | __all__ = ['logger'] 4 | 5 | logger = logging.getLogger("PY-PCQQ") 6 | logger.setLevel(logging.INFO) 7 | 8 | steram_handler = logging.StreamHandler() 9 | steram_formatter = logging.Formatter("[%(name)s] %(message)s") 10 | steram_handler.setFormatter(steram_formatter) 11 | logger.addHandler(steram_handler) --------------------------------------------------------------------------------