diff --git a/README.md b/README.md index 855b030..d1f5705 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,9 @@ ## 简介 -PS: 因协议版本已过期, 此版本协议库已无法使用 +因为之前用的轻聊版协议失效, 所以基于最新的 PCQQ9.7.5 协议重新开发 -PY-PCQQ 是一个基于 **QQ轻聊版 7.9** 客户端协议的 Python 异步 QQ 机器人支持库,它会对 QQ 服务端发出的协议包进行解析和处理,并以插件化的形式,分发给消息所对应的命令处理器。 +依赖的第三方库有: `cryptography`, `httpx`, `pillow` -除了起到解析消息的作用,PY-PCQQ 还通过 装饰器、异步、回调等方式实现了一套简洁易用的会话机制和插件机制,以便于用户快速上手。 +目前完善了一小部分, 可以 clone 本项目并运行 `example.py` 中的代码查看效果 -PY-PCQQ 在其底层与 QQ 服务端实现交互的部分使用的是标准库 `asyncio.open_connection` 所创建的异步 TCP 连接,这意味着在本协议库提供的内容多为异步操作,在调用相关函数或方法时应注意加上 **await** 关键字。 - -本项目基本仅由 Python3 的标准库所实现,但若是在没有图形界面的系统中使用扫码登录,需要自行安装第三方库 `pillow` 使得程序能在终端环境中打印登录二维码。值得一提的是,在 Android 手机等移动设备中,你也可以通过 [pydroid3](https://apkdownloadforandroid.com/ru.iiec.pydroid3/) 这样的应用来安装运行本协议库,例如: [在手机上玩转QQ机器人?](https://b23.tv/ZVHP0lK) - -最后要说的是,这个项目仅仅只是本废物空闲之余的兴趣之作,存在着大量不成熟与不完善的地方。如果对 QQ 机器人开发有所需求,可以移步至更加强大与稳定的 [mirai](https://github.com/mamoe/mirai) 或 [go-cqhttp](https://github.com/Mrs4s/go-cqhttp/) 等项目。 - -## 已实现功能 - -#### 登录 -- [x] 账号密码登录 -- [x] 二维码登录 -- [x] 本地Token重连 - -#### 发送消息 -- [x] At -- [x] 文本 -- [x] 表情 -- [x] xml卡片 -- [x] 图片 - -#### 接收消息 -- [x] At -- [x] 文本 -- [x] 图片 -- [x] 表情 - -#### 接收事件 -- [x] 群消息 -- [x] 好友消息 -- [x] 进群事件 -- [x] 退群事件 -- [x] 禁言事件 - -#### 其它操作 -- [x] 修改群成员Card -- [x] 设置群成员禁言 - -## 文档 -暂时咕咕中,请自行查看项目中的 `example.py` 查看案例 +估摸着这个垃圾玩意也不会有什么用户, 所以本项目将一直处于佛系更新中 \ No newline at end of file diff --git a/core/client.py b/core/client.py new file mode 100644 index 0000000..1c00a55 --- /dev/null +++ b/core/client.py @@ -0,0 +1,66 @@ +import os +import traceback + +from socket import inet_aton +from functools import partial +from asyncio import ( + Queue, + new_event_loop, + run_coroutine_threadsafe +) + +from core.entities import ( + Packet, + QQStruct, + PacketManger, +) + +from core import croto, const + +from core.utils import ( + UDPSocket, + rand_udp_host +) + + +class QQClient: + def __init__(self, uin: int = 0): + self.loop = new_event_loop() + + self.sock = UDPSocket( + host=rand_udp_host(), + port=const.UDP_PORT, + ) + self.manage = PacketManger() + + root = os.path.join(os.getcwd(), "data") + if uin != 0: + root = os.path.join(root, str(uin)) + + if not os.path.exists(root): + os.makedirs(root) + + self.stru = QQStruct( + path=root, + ecdh=croto.ECDH(), + addr=(self.sock.host, self.sock.port), + server_ip=inet_aton(self.sock.host) + ) + + self.run_task = self.loop.run_until_complete + self.add_task = partial(run_coroutine_threadsafe, loop=self.loop) + + def send(self, packet: Packet): + data = packet.encode() + self.sock.send(data) + + def recv(self, tea_key: bytes): + data = self.sock.recv() + return Packet.from_raw(data, tea_key) + + async def recv_and_exec(self, tea_key: bytes): + try: + packet = self.recv(tea_key) + await self.manage.exec_all(packet) + except Exception as err: + print('发生异常: ', traceback.format_exc()) \ No newline at end of file diff --git a/core/const.py b/core/const.py new file mode 100644 index 0000000..fde436a --- /dev/null +++ b/core/const.py @@ -0,0 +1,29 @@ +__bytes = bytes.fromhex + +Header = "02 3B 41" +Tail = "03" + +StructVersion = __bytes("03 00 00 00 01 01 01 00 00 6A 9C 00 00 00 00") +BodyVersion = __bytes("02 00 00 00 01 01 01 00 00 6A 9C") +FuncVersion = __bytes("04 00 00 00 01 01 01 00 00 6A 9C 00 00 00 00 00 00 00 00") +VMainVer = __bytes("3B 41") + +EcdhVersion = __bytes("01 03") +DWQDVersion = __bytes("04 04 04 00") +SsoVersion = __bytes("00 00 04 61") +ClientVersion = __bytes("00 00 17 41") + +RandKey = __bytes("66 D0 9F 63 A2 37 02 27 13 17 3B 1E 01 1C A9 DA") +ServiceId = __bytes("00 00 00 01") +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") + +TCP_PORT = 443 +UDP_PORT = 8000 +HeartBeatInterval = 30.0 + +StateOnline = 10 # 上线 +StateLeave = 30 # 离开 +StateInvisible = 40 # 隐身 +StateBusy = 50 # 忙碌 +StateCallMe = 60 # Q我吧 +StateUndisturb = 70 # 请勿打扰 \ No newline at end of file diff --git a/core/croto/__init__.py b/core/croto/__init__.py new file mode 100644 index 0000000..f33fd46 --- /dev/null +++ b/core/croto/__init__.py @@ -0,0 +1,17 @@ +from .ecdh import ECDH + +from .gid import gid_from_group + +from .hash import ( + md5, + sha256, + sha512, + sha1024, + rand_str, + rand_str2, + sub_16F90 +) + +from .offical import create_official + +from .qqtea import tea_encrypt, tea_decrypt diff --git a/core/croto/ecdh.py b/core/croto/ecdh.py new file mode 100644 index 0000000..49677c0 --- /dev/null +++ b/core/croto/ecdh.py @@ -0,0 +1,37 @@ +from cryptography.hazmat.bindings._openssl import ffi, lib + + +class ECDH: + def __init__(self): + self.public_key = bytes(25) + self.share_key = bytes(16) + + self.ec_key = lib.EC_KEY_new_by_curve_name(711) + self.group = lib.EC_KEY_get0_group(self.ec_key) + self.point = lib.EC_POINT_new(self.group) + + if lib.EC_KEY_generate_key(self.ec_key) == 1: + lib.EC_POINT_point2oct(self.group, lib.EC_KEY_get0_public_key( + self.ec_key), 2, self.public_key, len(self.public_key), ffi.NULL) + + buf = bytes([ + 4, 191, 71, 161, 207, 120, 166, + 41, 102, 139, 11, 195, 159, 142, + 84, 201, 204, 243, 182, 56, 75, + 8, 184, 174, 236, 135, 218, 159, + 48, 72, 94, 223, 231, 103, 150, + 157, 193, 163, 175, 17, 21, 254, + 13, 204, 142, 11, 23, 202, 207 + ]) + if lib.EC_POINT_oct2point(self.group, self.point, buf, len(buf), ffi.NULL) == 1: + lib.ECDH_compute_key(self.share_key, len( + self.share_key), self.point, self.ec_key, ffi.NULL) + + def twice(self, tk_key: bytes): + twice_key = bytes(16) + + if lib.EC_POINT_oct2point(self.group, self.point, tk_key, len(tk_key), ffi.NULL) == 1: + lib.ECDH_compute_key(twice_key, len(twice_key), + self.point, self.ec_key, ffi.NULL) + + return twice_key diff --git a/core/croto/gid.py b/core/croto/gid.py new file mode 100644 index 0000000..a5373bb --- /dev/null +++ b/core/croto/gid.py @@ -0,0 +1,40 @@ +def gid_from_group(group_id: int) -> int: + group = str(group_id) + left = int(group[0:-6]) + + if left >= 0 and left <= 10: + right = group[-6:] + gid = str(left + 202) + right + elif left >= 11 and left <= 19: + right = group[-6:] + gid = str(left + 469) + right + elif left >= 20 and left <= 66: + left = int(str(left)[0:1]) + right = group[-7:] + gid = str(left + 208) + right + elif left >= 67 and left <= 156: + right = group[-6:] + gid = str(left + 1943) + right + elif left >= 157 and left <= 209: + left = int(str(left)[0:2]) + right = group[-7:] + gid = str(left + 199) + right + elif left >= 210 and left <= 309: + left = int(str(left)[0:2]) + right = group[-7:] + gid = str(left + 389) + right + elif left >= 310 and left <= 335: + left = int(str(left)[0:2]) + right = group[-7:] + gid = str(left + 349) + right + elif left >= 336 and left <= 386: + left = int(str(left)[0:3]) + right = group[-6:] + gid = str(left + 2265) + right + elif left >= 387 and left <= 499: + left = int(str(left)[0:3]) + right = group[-6:] + gid = str(left + 3490) + right + elif left >= 500: + return int(group) + return int(gid) diff --git a/core/croto/hash.py b/core/croto/hash.py new file mode 100644 index 0000000..9cfb3db --- /dev/null +++ b/core/croto/hash.py @@ -0,0 +1,196 @@ +import ctypes +import random +import hashlib + + +def memwrite(dest: bytearray, i: int, value: bytes): + for i2 in range(len(value)): + dest[i + i2] = value[i2] + + +def memset(dest: bytearray, value: int): + for i in range(len(dest)): + dest[i] = value + +# ------------------------------------------ + + +def md5(src: bytes): + return hashlib.md5(src).digest() + + +def sha256(data: bytes, val: bytes): + v17 = bytearray(8) + + memwrite(v17, 0, (953751936).to_bytes(4, "little")) + memwrite(v17, 4, (27660).to_bytes(2, "little")) + memwrite(v17, 6, (28329).to_bytes(2, "little")) + + v7 = bytearray(hashlib.sha1(data).digest()) + v9 = len(v7) + + v8 = 0 + while v7 != bytearray(0): + b = v7[v8] ^ (val[v8 % 20] + v17[v8 % 7]) + v7[v8] = ctypes.c_ubyte(b).value + + v8 += 1 + if v8 >= v9: + break + + return bytes(v7) + + +def sha512(data: bytes, val: bytes): + v7 = bytearray(64) + v11 = bytearray(8) + v15 = bytearray(64) + v19 = bytearray(64) + + memwrite(v11, 0, (1980729049).to_bytes(4, "little")) + memwrite(v11, 4, (22840).to_bytes(2, "little")) + memwrite(v11, 6, (108).to_bytes(1, "little")) + + memwrite(v7, 0, val) + memset(v19, 22) + memset(v15, 60) + + for i in range(64): + v19[i] = v19[i] ^ v7[i] + v15[i] = v15[i] ^ v7[i] + + v18 = hashlib.sha256(bytes(v19) + data).digest() + v20 = bytearray(hashlib.sha256(v15 + v18[:36]).digest()) + + v9 = 0 + while v20 != bytearray(0): + b = v20[v9] ^ v11[v9 % 7] + v20[v9] = ctypes.c_ubyte(b).value + v9 += 1 + + if v9 >= 32: + break + + return bytes(v20) + + +def sha1024(data: bytes, val: bytes): + v22 = bytearray(7) + v25 = bytearray(5) + + memwrite(v22, 0, (249112998).to_bytes(4, "little")) + memwrite(v22, 4, (39555).to_bytes(2, "little")) + memwrite(v22, 6, (194).to_bytes(1, "little")) + + memwrite(v25, 0, (2840686918).to_bytes(4, "little")) + memwrite(v25, 4, (33).to_bytes(1, "little")) + + v16 = bytearray(hashlib.sha1(data).digest()) + v9 = len(v16) + v8 = 0 + + while v16 != bytearray(0): + b = v16[v8] ^ (v8 + val[v8 % 20] + v25[v8 % 5] + v22[v8 % 7]) + v16[v8] = ctypes.c_ubyte(b).value + + v8 += 1 + if v8 >= v9: + break + + return bytes(v16) + + +def rand_str(v1_3: bytearray): + v1 = bytearray(10) + memset(v1, 11) + + v3 = bytearray(20) + memset(v3, 12) + memwrite(v3, 0, (16204).to_bytes(2, "little")) + memwrite(v3, 2, (928279336).to_bytes(4, "little")) + memwrite(v3, 6, (610353194).to_bytes(4, "little")) + + v16 = bytearray(10) + memwrite(v16, 0, (24896).to_bytes(2, "little")) + memwrite(v16, 2, (910043961).to_bytes(4, "little")) + memwrite(v16, 6, (1327901016).to_bytes(4, "little")) + + v11 = bytes([97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 111, 112, 113, 114, 115, 116, 117, + 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]) + v2 = 3 + while True: + v1[v2] = v11[random.randint(0, 32767) % 44] + v2 += 1 + if v2 > 9: + break + v1[0] = v1[3] + v1[1] = v1[6] + v1[2] = v1[9] + + v7 = 7 + while True: + v8 = v7 - 7 + if v8 >= 7: + break + + v3[v7] = v16[v8] ^ v1[v8] + + v7 += 1 + if v7 >= 19: + break + v1_3.clear() + v1_3 += v3[-2:] + v3[:18] + return bytes(v1) + + +def rand_str2(v1_3: bytearray): + v1 = bytearray(10) + memset(v1, 13) + + v3 = bytearray(20) + memset(v3, 13) + memwrite(v3, 0, (16204).to_bytes(2, "little")) + memwrite(v3, 2, (928279336).to_bytes(4, "little")) + memwrite(v3, 6, (610353194).to_bytes(4, "little")) + + v16 = bytearray(10) + memwrite(v16, 0, (24896).to_bytes(2, "little")) + memwrite(v16, 2, (910043961).to_bytes(4, "little")) + memwrite(v16, 6, (1327901016).to_bytes(4, "little")) + + v11 = bytes([97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 111, 112, 113, 114, 115, 116, 117, + 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]) + v2 = 3 + while True: + v1[v2] = v11[random.randint(0, 32767) % 44] + v2 += 1 + if v2 > 9: + break + v1[0] = v1[3] + v1[1] = v1[6] + v1[2] = v1[9] + + v7 = 7 + while True: + v8 = v7 - 7 + if v8 >= 7: + break + + v3[v7] = v16[v8] ^ v1[v8] + + v7 += 1 + if v7 >= 19: + break + v1_3.clear() + v1_3 += v3[-2:] + v3[:18] + return bytes(v1) + + +def sub_16F90(data: bytes, v1: bytearray, fix: bytes, v1_3: bytes): + if len(v1) == 0: + v1 += bytearray(rand_str(v1_3)) + + v28 = sha256(data, fix) + v28 += sha512(data, fix) + + return v28[:40] diff --git a/core/croto/offical.py b/core/croto/offical.py new file mode 100644 index 0000000..34adbe1 --- /dev/null +++ b/core/croto/offical.py @@ -0,0 +1,118 @@ +import ctypes +from hashlib import md5 + + +def ulong_overflow(val): + MAX = 2147483647 + if not -MAX-1 <= val <= MAX: + val = (val + (MAX + 1)) % (2 * (MAX + 1)) - MAX - 1 + return ctypes.c_ulong(val).value + + +def offical(data, key): + eax = int.from_bytes(data[0:4], "big") + esi = int.from_bytes(data[4:8], "big") + + var4 = int.from_bytes(key[0:4][::-1], "big") + ebp = int.from_bytes(key[4:8][::-1], "big") + var3 = int.from_bytes(key[8:12][::-1], "big") + var2 = int.from_bytes(key[12:16][::-1], "big") + + edi = 0x9E3779B9 + ecx = edx = 0 + + for _ in range(16): + edx = esi + ecx = esi + + edx = ulong_overflow(edx >> 5) + ecx = ulong_overflow(ecx << 4) + + edx = ulong_overflow(edx + ebp) + ecx = ulong_overflow(ecx + var4) + + edx = ulong_overflow(edx ^ ecx) + ecx = ulong_overflow(esi + edi) + + edx = ulong_overflow(edx ^ ecx) + eax = ulong_overflow(eax + edx) + edx = eax + ecx = eax + + edx = ulong_overflow(edx << 4) + edx = ulong_overflow(edx + var3) + ecx = ulong_overflow(ecx >> 5) + ecx = ulong_overflow(ecx + var2) + + edx = ulong_overflow(edx ^ ecx) + ecx = ulong_overflow(eax + edi) + edx = ulong_overflow(edx ^ ecx) + edi = edi - 0x61C88647 + esi = ulong_overflow(esi + edx) + + return eax.to_bytes(4, "big") + esi.to_bytes(4, "big") + + +def create_official(bufKey, bufSig, tgt_encrypt): + MD5InfoCount = 4 + round = 256 + TmOffMod = 19 + TmOffModAdd = 5 + md5info = md5(bufKey).digest() + md5info = md5info + md5(bufSig).digest() + keyround = 480 % TmOffMod + TmOffModAdd + seq = bytearray(256) + ls = bytearray(256) + off = bytearray(16) + + for i in range(round): + seq[i] = i + ls[i] = md5info[16 + (i % 16)] + + x = 0 + for i in range(round): + x = (x + seq[i] + ls[i]) % round + m = seq[x] + seq[x] = seq[i] + seq[i] = m + + x = 0 + for i in range(16): + x = (x + seq[i + 1]) % round + m = seq[x] + seq[x] = seq[i + 1] + seq[i + 1] = m + + v = (seq[x] + seq[i + 1]) % round + 1 + md5info = md5info + bytes([seq[v - 1] ^ md5info[i]]) + + md5info = md5info + md5(tgt_encrypt).digest() + MD5MD5info = md5(md5info).digest() + M0 = MD5MD5info + for i in range(keyround): + M0 = md5(M0).digest() + md5info = M0 + md5info[16:] + + t1 = MD5MD5info[:8] + t2 = MD5MD5info[8:] + + for i in range(MD5InfoCount): + lp = i * 16 + prekey = md5info[lp:] + prekey = prekey[:17] + + KEY = bytes(0) + a = 0 + while a < 16: + KEY = KEY + bytes([prekey[a + 3]]) + bytes([prekey[a + 2]]) + \ + bytes([prekey[a + 1]]) + bytes([prekey[a + 0]]) + a += 4 + + ii = offical(t1, KEY) + offical(t2, KEY) + b = i + + while b < 16: + off[b] = off[b] ^ ii[b] + b += 1 + + return md5(off).digest() diff --git a/pcqq/binary/qqtea.py b/core/croto/qqtea.py similarity index 54% rename from pcqq/binary/qqtea.py rename to core/croto/qqtea.py index 2b26ed1..3dd3f1f 100644 --- a/pcqq/binary/qqtea.py +++ b/core/croto/qqtea.py @@ -1,48 +1,6 @@ import struct -class QQTea: - def __init__(self, key: bytes) -> None: - self.key = key - - def encrypt(self, src: bytes) -> bytes: - END_CHAR = b'\0' - FILL_N_OR = 0xF8 - vl = len(src) - filln = (8 - (vl + 2)) % 8 + 2 - fills = b'' - for i in range(filln): - fills = fills + bytes([220]) - src = (bytes([(filln - 2) | FILL_N_OR]) - + fills - + src - + END_CHAR * 7) - tr = b'\0' * 8 - to = b'\0' * 8 - r = b'' - o = b'\0' * 8 - for i in range(0, len(src), 8): - o = xor(src[i:i + 8], tr) - tr = xor(code(o, self.key), to) - to = o - r += tr - return r - - def decrypt(self, src: bytes) -> bytes: - l = len(src) - prePlain = decipher(src, self.key) - pos = (prePlain[0] & 0x07) + 2 - r = prePlain - preCrypt = src[0:8] - for i in range(8, l, 8): - x = xor(decipher(xor(src[i:i + 8], prePlain), self.key), preCrypt) - prePlain = xor(x, preCrypt) - preCrypt = src[i:i + 8] - r += x - if r[-7:] != b'\0' * 7: - return None - return r[pos + 1:-7] - def xor(a, b) -> bytes: op = 0xffffffff @@ -83,3 +41,42 @@ def decipher(v, k): s -= delta s &= op return struct.pack(b'>LL', y, z) + + +def tea_encrypt(data: bytes, key: bytes): + END_CHAR = b'\0' + FILL_N_OR = 0xF8 + vl = len(data) + filln = (8 - (vl + 2)) % 8 + 2 + fills = b'' + for i in range(filln): + fills = fills + bytes([220]) + data = (bytes([(filln - 2) | FILL_N_OR]) + + fills + + data + + END_CHAR * 7) + tr = b'\0' * 8 + to = b'\0' * 8 + r = b'' + o = b'\0' * 8 + for i in range(0, len(data), 8): + o = xor(data[i:i + 8], tr) + tr = xor(code(o, key), to) + to = o + r += tr + return r + +def tea_decrypt(data: bytes, key:bytes): + l = len(data) + prePlain = decipher(data, key) + pos = (prePlain[0] & 0x07) + 2 + r = prePlain + preCrypt = data[0:8] + for i in range(8, l, 8): + x = xor(decipher(xor(data[i:i + 8], prePlain), key), preCrypt) + prePlain = xor(x, preCrypt) + preCrypt = data[i:i + 8] + r += x + if r[-7:] != b'\0' * 7: + return None + return r[pos + 1:-7] \ No newline at end of file diff --git a/core/entities/__init__.py b/core/entities/__init__.py new file mode 100644 index 0000000..df1694f --- /dev/null +++ b/core/entities/__init__.py @@ -0,0 +1,20 @@ +from .event import ( + NoticeEvent, + MessageEvent, +) + +from .message import ( + Message, + TextNode, + AtNode, + FaceNode, + ImageNode +) + +from .packet import ( + Packet, + PacketHandler, + PacketManger +) + +from .struct import QQStruct \ No newline at end of file diff --git a/core/entities/event.py b/core/entities/event.py new file mode 100644 index 0000000..e1ed167 --- /dev/null +++ b/core/entities/event.py @@ -0,0 +1,46 @@ +from .message import Message + +from dataclasses import dataclass + + +@dataclass +class BaseEvent: + time: int = 0 + + self_id: int = 0 + + post_type: str = "" + + +@dataclass +class MessageEvent(BaseEvent): + post_type: str = "message" + + message_type: str = "" + + sub_type: str = "" + + message_id: int = 0 + + message_num: int = 0 + + group_id: int = 0 + + user_id: int = 0 + + message: Message = None + + +@dataclass +class NoticeEvent(BaseEvent): + post_type: str = "notice" + + notice_type: str = "" + + sub_type: str = "" + + group_id: int = 0 + + operator_id: int = 0 + + user_id: int = 0 diff --git a/core/entities/message.py b/core/entities/message.py new file mode 100644 index 0000000..06b3247 --- /dev/null +++ b/core/entities/message.py @@ -0,0 +1,197 @@ +from typing import List, Union +from dataclasses import dataclass +from abc import ABCMeta, abstractmethod + +from core import utils + + +class BaseNode(metaclass=ABCMeta): + @abstractmethod + def format(self) -> str: + """将该 Node 格式化为字符串""" + pass + + @abstractmethod + def encode(self) -> bytes: + """将该 Node 编码成对应的协议源数据""" + pass + + @abstractmethod + def from_raw(self, raw_data: bytes): + """解析协议源数据并返回对应的 Node 对象""" + pass + + +@dataclass +class TextNode(BaseNode): + text: str + + def format(self) -> str: + return self.text + + def encode(self) -> bytes: + stream = utils.Stream() + body = self.text.encode() + + stream.write_byte(0x01) + stream.write_int16(len(body) + 3) + stream.write_byte(0x01) + stream.write_int16(len(body)) + stream.write(body) + + return stream.read_all() + + def from_raw(raw_data: bytes): + stream = utils.Stream(raw_data) + text = stream.del_left(1).read_token().decode() + + if len(stream._raw) > 0: + return AtNode.from_raw(raw_data) + + return TextNode(text) + + +@dataclass +class AtNode(BaseNode): + uin: int + name: str + + def format(self) -> str: + return f"[PQ:at,qq={self.uin},name={self.name}]" + + def encode(self) -> bytes: + stream = utils.Stream() + name = "@" + self.name + + stream.write_hex("00 01 00 00") + stream.write_int16(len(name)) + stream.write_hex("00") + stream.write_int32(self.uin) + stream.write_hex("00 00") + body = stream.read_all() + + name = name.encode() + stream.write_byte(0x01) + stream.write_int16(len(name)) + stream.write(name) + stream.write_byte(0x06) + stream.write_int16(len(body)) + stream.write(body) + body = stream.read_all() + + stream.write_byte(0x01) + stream.write_int16(len(body)) + stream.write(body) + + return stream.read_all() + + def from_raw(raw_data: bytes): + stream = utils.Stream(raw_data) + + stream.del_left(1) + name = stream.read_token().decode()[1:] + + stream.del_left(10) + uin = stream.read_int32() + + return AtNode(uin, name) + + +@dataclass +class FaceNode(BaseNode): + id: int + + def format(self) -> str: + return f"[PQ:face,id={self.id}]" + + def encode(self) -> bytes: + stream = utils.Stream() + + stream.write_byte(0x02) + stream.write_int16(1 + 3) + stream.write_byte(0x01) + stream.write_int16(1) + stream.write_byte(self.id) + + return stream.read_all() + + def from_raw(raw_data: bytes): + stream = utils.Stream(raw_data) + face_id = stream.del_left(3).read_byte() + + return FaceNode(face_id) + + +@dataclass +class ImageNode(BaseNode): + hash: str + + def format(self) -> str: + return f"[PQ:image,url=https://gchat.qpic.cn/gchatpic_new/0/0-0-{self.hash}/0?term=3]" + + def encode(self) -> bytes: + return b'' + + def from_raw(raw_data: bytes): + stream = utils.Stream(raw_data) + + stream.del_left(1) + uuid = stream.read_token().decode().strip(" {}") + hash = uuid.replace("-", "").upper()[:32] + + return ImageNode(hash) + + +MessageNode = Union[TextNode, AtNode, FaceNode, ImageNode] + + +class Message: + def __init__(self): + self.__nodes: List[MessageNode] = [] + + def add(self, node: MessageNode): + if isinstance(node, TextNode): + pass + elif isinstance(node, AtNode): + pass + elif isinstance(node, FaceNode): + pass + elif isinstance(node, ImageNode): + pass + else: + raise ValueError("Message 添加的元素不是 MessageNode 包括的类型") + + self.__nodes.append(node) + return self + + def format(self): + format = "" + + for node in self.__nodes: + format += node.format() + return format + + def encode(self): + raw_data = b'' + for node in self.__nodes: + raw_data += node.encode() + return raw_data + + def from_raw(raw_data: bytes): + message = Message() + stream = utils.Stream(raw_data) + + while len(stream._raw) > 3: + node_type = stream.read_byte() + node_raw = stream.read_token() + + if node_type == 0x01: # 文本消息 + message.add(TextNode.from_raw(node_raw)) + elif node_type == 0x02: # 表情消息 + message.add(FaceNode.from_raw(node_raw)) + elif node_type == 0x03: # 群图片消息 + message.add(ImageNode.from_raw(node_raw)) + elif node_type == 0x06: # 私聊图片消息 + message.add(ImageNode.from_raw(node_raw)) + + return message \ No newline at end of file diff --git a/core/entities/packet.py b/core/entities/packet.py new file mode 100644 index 0000000..87ac831 --- /dev/null +++ b/core/entities/packet.py @@ -0,0 +1,103 @@ +from dataclasses import dataclass +from typing import Any, List, Callable, Coroutine + +from core import croto +from core.const import Header, Tail +from core.utils import Stream, rand_bytes + + +@dataclass +class Packet: + header: str = Header + + cmd: str = "" + + sequence: bytes = rand_bytes(2) + + uin: int = 0 + + version: Stream = Stream() + + body: Stream = Stream() + + tail: str = Tail + + tea_key: bytes = b'' + + def encode(self): + stream = Stream() + + stream.write_hex(self.header) + + stream.write_hex(self.cmd) + stream.write(self.sequence) + stream.write_int32(self.uin) + + stream.write_stream(self.version) + if self.tea_key == b'': + stream.write_stream(self.body) + else: + data = self.body.read_all() + stream.write(croto.tea_encrypt(data, self.tea_key)) + + stream.write_hex(self.tail) + + return stream.read_all() + + def from_raw(raw_data: bytes, tea_key: bytes): + stream = Stream(raw_data) + stream.del_left(3).del_right(1) + + packet = Packet( + cmd=stream.read_hex(2), + sequence=stream.read(2), + uin=stream.read_int32(), + tea_key=tea_key + ) + + stream.del_left(3) + if packet.tea_key == b'': + packet.body = stream + else: + data = croto.tea_decrypt(stream._raw, tea_key) + packet.body = Stream(data) + + return packet + + +@dataclass +class PacketHandler: + temp: bool + + check: Callable[[Packet], bool] + + handle: Callable[[Packet], Coroutine[Any, Any, None]] + + +class PacketManger(List[PacketHandler]): + def __repr__(self) -> str: + return "PacketManger{\n%s\n}" % ("\n\n".join(["\t" + str(phr) for phr in self])) + + def append(self, phr: PacketHandler) -> None: + if not isinstance(phr, PacketHandler): + raise ValueError("PacketManger 添加的元素不是 PacketHandler 类型") + + if not phr in self: + super().append(phr) + + return self + + def pop(self, phr: PacketHandler): + if phr in self: + super().pop(self.index(phr)) + + return self + + async def exec_all(self, packet: Packet): + for phr in self: + if not phr.check(packet): + continue + + if phr.temp: + self.pop(phr) + await phr.handle(packet) diff --git a/core/entities/struct.py b/core/entities/struct.py new file mode 100644 index 0000000..b684ba1 --- /dev/null +++ b/core/entities/struct.py @@ -0,0 +1,61 @@ +from dataclasses import dataclass + +from core import croto + +@dataclass +class QQStruct: + addr: tuple = () + + local_ip: bytes = b'' + + server_ip: bytes = b'' + + uin: int = 0 + + password: bytes = b'' + + nickname: str = '' + + is_scancode = True + + is_running = False + + login_time: bytes = b'' + + ecdh: croto.ECDH = None + + path: str = "" + + redirection_times: int = 0 + + redirection_history: bytes = b'' + + tgt_key: bytes = b'' + + tgtgt_key: bytes = b'' + + session_key: bytes = b'' + + offical: bytes = b'' + + bkn: int = 0 + + skey: str = '' + + cookie: str = '' + + pckey_for_0819: bytes = b'' + + pckey_for_0828_send: bytes = b'' + + pckey_for_0828_recv: bytes = b'' + + token_by_scancode: bytes = b'' + + token_0038_from_0825: bytes = b'' + + token_0038_from_0818: bytes = b'' + + token_0038_from_0836: bytes = b'' + + token_0088_from_0836: bytes = b'' \ No newline at end of file diff --git a/core/protocol/event/__init__.py b/core/protocol/event/__init__.py new file mode 100644 index 0000000..133b4fd --- /dev/null +++ b/core/protocol/event/__init__.py @@ -0,0 +1,3 @@ +from .group_msg import handle_group_msg +from .msg_receipt import handle_msg_receipt +from .private_msg import handle_private_msg \ No newline at end of file diff --git a/core/protocol/event/group_msg.py b/core/protocol/event/group_msg.py new file mode 100644 index 0000000..a992df4 --- /dev/null +++ b/core/protocol/event/group_msg.py @@ -0,0 +1,65 @@ +from typing import Coroutine, Callable +from logger import logger + +from core.protocol import webapi +from core.client import QQClient +from core.entities import ( + Packet, + PacketHandler, + MessageEvent, + Message, +) + +from .msg_receipt import pack_0002_receipt + +def handle_group_msg(cli: QQClient, func: Callable[[Message], Coroutine]): + def check_0017(packet: Packet): + if packet.cmd != "00 17": + return False + + return packet.body._raw[18:20] == bytes.fromhex("00 52") + + async def handle_0017(packet: Packet): + event = MessageEvent( + message_type="group", + sub_type="normal" + ) + + body = packet.body + guid = body.read_int32() + event.self_id = body.read_int32() + + body.del_left(12) + body.read(body.read_int32()) + + event.group_id = body.read_int32() + event.user_id = body.del_left(1).read_int32() + + event.message_id = body.read_int32() + event.time = body.read_int32() + + body.del_left(8) + body.del_left(1).del_left(1).del_left(2) + body.del_left(12) + + send_time = body.read_int32() + event.message_num = body.read_int32() + + body.del_left(8).read_token() + body.del_left(2) + event.raw_message = body._raw + + event.message = Message.from_raw(body.read_all()) + + group = await webapi.get_group(cli, event.group_id) + member = group.members[str(event.user_id)] + cli.send(pack_0002_receipt(cli, event)) + + logger.info(f"收到群聊 {group.name}({event.group_id})内 {member.card}({event.user_id})消息: {event.message.format()}") + cli.add_task(func(event)) + + return PacketHandler( + temp=False, + check=check_0017, + handle=handle_0017 + ) diff --git a/core/protocol/event/msg_receipt.py b/core/protocol/event/msg_receipt.py new file mode 100644 index 0000000..1aac4f8 --- /dev/null +++ b/core/protocol/event/msg_receipt.py @@ -0,0 +1,71 @@ +from core import const, croto +from core.client import QQClient +from core.entities import ( + Packet, + PacketHandler, + MessageEvent, +) + + +def pack_0002_receipt(cli: QQClient, event: MessageEvent): + packet = Packet( + cmd="00 02", + uin=cli.stru.uin, + tea_key=cli.stru.session_key + ) + + packet.version.write(const.BodyVersion) + packet.body.write_byte(41) + packet.body.write_int32(croto.gid_from_group(event.group_id)) + packet.body.write_byte(2) + packet.body.write_int32(event.message_id) + + return packet + + +def pack_0319_receipt(cli: QQClient, event: MessageEvent): + packet = Packet( + cmd="03 19", + uin=cli.stru.uin, + tea_key=cli.stru.session_key + ) + + packet.version.write(const.FuncVersion) + + packet.body.write_hex("08 01") + packet.body.write_hex("12 03 98 01 00") + packet.body.write_hex("0A 0E 08") + packet.body.write_varint(event.user_id) + packet.body.write_hex("10") + packet.body.write_varint(event.time) + packet.body.write_hex("20 00") + data = packet.body.read_all() + + packet.body.write_hex("00 00 00 07") + packet.body.write_int32(len(data)-7) + packet.body.write(data) + + return packet + + +def handle_msg_receipt(cli: QQClient): + def check_0017_00ce(packet: Packet): + return packet.cmd in ["00 17", "00 CE", "03 55"] + + async def handle_0017_00ce(packet: Packet): + receipt_packet = Packet( + cmd=packet.cmd, + uin=cli.stru.uin, + sequence=packet.sequence, + tea_key=cli.stru.session_key + ) + + receipt_packet.version.write(const.BodyVersion) + receipt_packet.body.write(packet.body._raw[0:16]) + cli.send(receipt_packet) + + return PacketHandler( + temp=False, + check=check_0017_00ce, + handle=handle_0017_00ce + ) diff --git a/core/protocol/event/private_msg.py b/core/protocol/event/private_msg.py new file mode 100644 index 0000000..d7bb14f --- /dev/null +++ b/core/protocol/event/private_msg.py @@ -0,0 +1,64 @@ +from typing import Coroutine, Callable +from logger import logger + +from core.protocol import webapi +from core.client import QQClient +from core.entities import ( + Packet, + PacketHandler, + MessageEvent, + Message +) + +from .msg_receipt import pack_0319_receipt + + +def handle_private_msg(cli: QQClient, func: Callable[[Message], Coroutine]): + def check_00ce(packet: Packet): + if packet.cmd != "00 CE": + return False + + return packet.body._raw[18:20] == bytes.fromhex("00 A6") + + async def handle_00ce(packet: Packet): + event = MessageEvent( + message_type="private", + sub_type="normal" + ) + + body = packet.body + event.user_id = body.read_int32() + event.self_id = body.read_int32() + + body.del_left(12) + body.del_left(body.read_int32() + 26) + + if body.read_hex(2) == "00 AF": + pass # 好友抖动 + + event.message_id = body.read_int16() + event.time = body.read_int32() + + body.del_left(6).del_left(1).del_left(1) + body.del_left(2).del_left(9) + + send_time = body.read_int32() + event.message_num = body.read_int32() + + body.del_left(8).read_token() + body.read_int16() + + event.message = Message.from_raw(body.read_all()) + user = await webapi.get_user(cli, event.user_id) + + cli.send(pack_0319_receipt(cli, event)) + + logger.info( + f"收到私聊 {user.name}({event.user_id}) 消息: {event.message.format()}") + cli.add_task(func(event)) + + return PacketHandler( + temp=False, + check=check_00ce, + handle=handle_00ce + ) diff --git a/core/protocol/pcapi/__init__.py b/core/protocol/pcapi/__init__.py new file mode 100644 index 0000000..efa79b9 --- /dev/null +++ b/core/protocol/pcapi/__init__.py @@ -0,0 +1 @@ +from .send_msg import send_group_msg, send_private_msg \ No newline at end of file diff --git a/core/protocol/pcapi/send_msg.py b/core/protocol/pcapi/send_msg.py new file mode 100644 index 0000000..d6dd4f4 --- /dev/null +++ b/core/protocol/pcapi/send_msg.py @@ -0,0 +1,65 @@ +from time import time + +from core import const, utils, croto +from core.client import QQClient +from core.entities import Packet, Message + +def send_group_msg(cli: QQClient, group_id:int, message: Message): + packet = Packet( + cmd="00 02", + uin=cli.stru.uin, + tea_key=cli.stru.session_key + ) + packet.version.write(const.BodyVersion) + + body = message.encode() + timestamp = int(time()).to_bytes(4, 'big') + + packet.body.write_hex("00 01 01 00 00 00 00 00 00 00") + packet.body.write_hex("4D 53 47 00 00 00 00 00") + + packet.body.write(timestamp) + packet.body.write(utils.rand_bytes(4)) + + packet.body.write_hex("00 00 00 00 09 00 86 00 00") + packet.body.write_hex("06 E5 AE 8B E4 BD 93 00 00") + packet.body.write(body) + body = packet.body.read_all() + + packet.body.write_hex("2A") + packet.body.write_int32(croto.gid_from_group(group_id)) + packet.body.write_int16(len(body)) + packet.body.write(body) + cli.send(packet) + +def send_private_msg(cli: QQClient, user_id:int, message: Message): + packet = Packet( + cmd="00 CD", + uin=cli.stru.uin, + tea_key=cli.stru.session_key + ) + packet.version.write_hex("03 00 00 00 01 01 01 00 00 6A 9C 75 37 7D 94") + + msg_data = message.encode() + timestamp = int(time()).to_bytes(4, "big") + + packet.body.write_int32(cli.stru.uin) + packet.body.write_int32(user_id) + + packet.body.write_hex("00 00 00 08 00 01 00 04 00 00 00 00") + packet.body.write(const.VMainVer) + + packet.body.write_int32(cli.stru.uin) + packet.body.write_int32(user_id) + packet.body.write(croto.md5(user_id.to_bytes(4, "big") + cli.stru.session_key)) + + packet.body.write_hex("00 0B") + packet.body.write(utils.rand_bytes(2)) + packet.body.write(timestamp) + + packet.body.write_hex("00 00 00 00 00 00 01 00 00 00 01 4D 53 47 00 00 00 00 00") + packet.body.write(timestamp) + packet.body.write(utils.rand_bytes(4)) + 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") + packet.body.write(msg_data) + cli.send(packet) \ No newline at end of file diff --git a/core/protocol/webapi/__init__.py b/core/protocol/webapi/__init__.py new file mode 100644 index 0000000..d755415 --- /dev/null +++ b/core/protocol/webapi/__init__.py @@ -0,0 +1,8 @@ +from .custom_api import get_user, get_group + +from .offical_api import ( + search_v3, + get_group_info_all, + get_group_members_new, + set_group_card +) diff --git a/core/protocol/webapi/custom_api.py b/core/protocol/webapi/custom_api.py new file mode 100644 index 0000000..0cfbae5 --- /dev/null +++ b/core/protocol/webapi/custom_api.py @@ -0,0 +1,114 @@ +import os +from typing import Dict, List +from dataclasses import dataclass +from jsondataclass import from_json, to_json + +from core.client import QQClient +from .offical_api import ( + cgi_get_score, + search_v3, + get_group_info_all, + get_group_members_new, +) + + +@dataclass +class QQUser: + name: str + sex: str + area: str + + +@dataclass +class QQMember: + name: str + card: str + role: str + + +@dataclass +class QQGroup: + name: str + owner: int + admins: List[int] + members: Dict[str, QQMember] + + +QQUserCache: Dict[str, QQUser] = {} +QQGroupCache: Dict[str, QQGroup] = {} + + +async def get_user(cli: QQClient, user_id: int) -> QQUser: + global QQUserCache + user_id = str(user_id) + path = os.path.join(cli.stru.path, "QQUser.json") + + if QQUserCache == {} and os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + QQUserCache = from_json(f.read(), Dict[str, QQUser]) + + + if user_id in QQUserCache: + return QQUserCache[user_id] + + ret = await search_v3(cli.stru, user_id) + if ret == None: + ret = await cgi_get_score(user_id) + QQUserCache[user_id] = QQUser( + name=ret[user_id][-4], + sex="unknow", + area="", + ) + else: + QQUserCache[user_id] = QQUser( + name=ret["nick"], + sex={1: "male", 2: "female"}.get(ret["gender"], "unknow"), + area=ret["country"], + ) + + with open(path, "w", encoding="utf-8") as f: + f.write(to_json(QQUserCache, ensure_ascii=False)) + + return QQUserCache[user_id] + + +async def get_group(cli: QQClient, group_id: int): + global QQUserCache, QQGroupCache + group_id = str(group_id) + path = os.path.join(cli.stru.path, f"{group_id}.json") + + if not group_id in QQGroupCache and os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + QQGroupCache[group_id] = from_json(f.read(), QQGroup) + + if group_id in QQGroupCache: + return QQGroupCache[group_id] + + ret = await get_group_info_all(cli.stru, group_id) + group = QQGroup( + name=ret["gName"], + owner=ret["gOwner"], + admins=ret["gAdmins"], + members=dict() + ) + + ret = await get_group_members_new(cli.stru, group_id) + for mem in ret["mems"]: + if mem['u'] == group.owner: + mem_role = "owner" + elif mem['u'] in group.admins: + mem_role = "admin" + else: + mem_role = "member" + + group.members[str(mem['u'])] = QQMember( + name=mem["n"], + card=ret['cards'].get(str(mem['u']), mem["n"]), + role=mem_role, + ) + + QQGroupCache[group_id] = group + with open(path, "w", encoding="utf-8") as f: + f.write(to_json(QQGroupCache[group_id], ensure_ascii=False)) + + return QQGroupCache[group_id] diff --git a/core/protocol/webapi/offical_api.py b/core/protocol/webapi/offical_api.py new file mode 100644 index 0000000..abeb56b --- /dev/null +++ b/core/protocol/webapi/offical_api.py @@ -0,0 +1,74 @@ +import httpx +import json + +from core.entities import QQStruct + +async def cgi_get_score(user_id:int) -> dict: + async with httpx.AsyncClient() as client: + rsp = await client.get(f"https://r.qzone.qq.com/fcg-bin/cgi_get_score.fcg?mask=7&uins={user_id}") + + return json.loads(rsp.text.strip('portraitCallBack();')) + +async def search_v3(stru: QQStruct, user_id: int): + async with httpx.AsyncClient() as client: + rsp = await client.get( + url="https://cgi.find.qq.com/qqfind/buddy/search_v3", + params={"keyword": user_id}, + headers={"Cookie": stru.cookie} + ) + + try: + ret = rsp.json() + return ret["result"]["buddy"]["info_list"][0] + except: + return None + + +async def get_group_info_all(stru: QQStruct, group_id: int): + async with httpx.AsyncClient() as client: + rsp = await client.get( + url="https://qinfo.clt.qq.com/cgi-bin/qun_info/get_group_info_all", + params={ + "gc": group_id, + "bkn": stru.bkn, + "src": "qinfo_v3", + }, + headers={"Cookie": stru.cookie} + ) + + return rsp.json() + + +async def get_group_members_new(stru: QQStruct, group_id: int): + async with httpx.AsyncClient() as client: + rsp = await client.get( + url="https://qinfo.clt.qq.com/cgi-bin/qun_info/get_group_members_new", + params={ + "gc": group_id, + "bkn": stru.bkn, + "src": "qinfo_v3", + }, + headers={"Cookie": stru.cookie} + ) + + return rsp.json() + +# Success: {'ec': 0, 'errcode': 0, 'em': 'ok'} +# Failed: {'ec': 3, 'errcode': 0, 'em': ''} + + +async def set_group_card(stru: QQStruct, group_id: int, user_id: int, card: str): + async with httpx.AsyncClient() as client: + rsp = await client.post( + url="https://qinfo.clt.qq.com/cgi-bin/qun_info/set_group_card", + data={ + "u": user_id, + "gc": group_id, + "bkn": stru.bkn, + "src": "qinfo_v3", + "name": card + }, + headers={"Cookie": stru.cookie} + ) + + return rsp.json() diff --git a/core/protocol/wtlogin/__init__.py b/core/protocol/wtlogin/__init__.py new file mode 100644 index 0000000..a277683 --- /dev/null +++ b/core/protocol/wtlogin/__init__.py @@ -0,0 +1,3 @@ +from .wtlogin import login_out, login_by_scan, login_by_token + +__all__ = ["login_out", "login_by_scan", "login_by_token"] \ No newline at end of file diff --git a/core/protocol/wtlogin/p1_0825_ping/__init__.py b/core/protocol/wtlogin/p1_0825_ping/__init__.py new file mode 100644 index 0000000..9be9555 --- /dev/null +++ b/core/protocol/wtlogin/p1_0825_ping/__init__.py @@ -0,0 +1,2 @@ +from .pack import pack_0825 +from .unpack import unpack_0825 \ No newline at end of file diff --git a/core/protocol/wtlogin/p1_0825_ping/pack.py b/core/protocol/wtlogin/p1_0825_ping/pack.py new file mode 100644 index 0000000..d7b8716 --- /dev/null +++ b/core/protocol/wtlogin/p1_0825_ping/pack.py @@ -0,0 +1,59 @@ +from core import utils +from core import const +from core.entities import Packet, QQStruct + + +def pack_0825(stru: QQStruct): + packet = Packet( + cmd="08 25", + uin=stru.uin, + tea_key=const.RandKey + ) + packet.version.write(const.StructVersion).write(const.RandKey) + + def tlv_018(ns: utils.Stream): + ns.write_hex("00 01") + ns.write(const.SsoVersion) + ns.write(const.ServiceId) + ns.write(const.ClientVersion) + ns.write_int32(stru.uin) + ns.write_int16(stru.redirection_times) + ns.write_hex("00 00") + + ns.write_tlv("00 18", ns.read_all()) + packet.body.write_func(tlv_018) + + def tlv_309(ns: utils.Stream): + ns.write_hex("00 0A 00 04") + ns.write_hex("00 00 00 00") + + if stru.redirection_times > 0: + ns.write_byte(stru.redirection_times) + ns.write_hex("00 04") + ns.write(stru.redirection_history) + ns.write_hex("00 04") + + ns.write_tlv("03 09", ns.read_all()) + packet.body.write_func(tlv_309) + + def tlv_036(ns: utils.Stream): + ns.write_hex("00 02 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00") + + ns.write_tlv("00 36", ns.read_all()) + packet.body.write_func(tlv_036) + + def tlv_114(ns: utils.Stream): + ns.write(const.EcdhVersion) + + ns.write_token(stru.ecdh.public_key) + + ns.write_tlv("01 14", ns.read_all()) + packet.body.write_func(tlv_114) + + def tlv_511(ns: utils.Stream): + ns.write_hex("0A 00 00 00 00 01") + + ns.write_tlv("05 11", ns.read_all()) + packet.body.write_func(tlv_511) + + return packet diff --git a/core/protocol/wtlogin/p1_0825_ping/unpack.py b/core/protocol/wtlogin/p1_0825_ping/unpack.py new file mode 100644 index 0000000..ed2cb9d --- /dev/null +++ b/core/protocol/wtlogin/p1_0825_ping/unpack.py @@ -0,0 +1,39 @@ +from functools import partial + +from logger import logger +from core.entities import QQStruct, Packet, PacketHandler + + +def check_0825(packet: Packet): + return packet.cmd == "08 25" + + +async def handle_0825(stru: QQStruct, packet: Packet): + stream = packet.body + + sign = stream.read_byte() + stream.read(2) + stru.token_0038_from_0825 = stream.read(stream.read_int16()) + stream.read(6) + stru.login_time = stream.read(4) + stru.local_ip = stream.read(4) + stream.read(2) + + logger.info(f"已选用 {stru.addr[0]} 作为登录服务器") + if sign == 0xfe: + stream.read(18) + stru.server_ip = stream.read(4) + stru.redirection_times += 1 + stru.redirection_history += stru.server_ip + elif sign == 0x00: + stream.read(6) + stru.server_ip = stream.read(4) + stru.redirection_history = b'' + + +def unpack_0825(stru: QQStruct): + return PacketHandler( + temp=True, + check=check_0825, + handle=partial(handle_0825, stru) + ) diff --git a/core/protocol/wtlogin/p2_0818_qrcode/__init__.py b/core/protocol/wtlogin/p2_0818_qrcode/__init__.py new file mode 100644 index 0000000..ea97c2f --- /dev/null +++ b/core/protocol/wtlogin/p2_0818_qrcode/__init__.py @@ -0,0 +1,2 @@ +from .pack import pack_0818 +from .unpack import unpack_0818 \ No newline at end of file diff --git a/core/protocol/wtlogin/p2_0818_qrcode/pack.py b/core/protocol/wtlogin/p2_0818_qrcode/pack.py new file mode 100644 index 0000000..2ab81fc --- /dev/null +++ b/core/protocol/wtlogin/p2_0818_qrcode/pack.py @@ -0,0 +1,37 @@ +from core import utils +from core import const +from core.entities import Packet, QQStruct + + +def pack_0818(stru: QQStruct): + packet = Packet( + cmd="08 18", + tea_key=const.RandKey + ) + packet.version.write(const.StructVersion).write(const.RandKey) + + def tlv_019(ns: utils.Stream): + ns.write_hex("00 01") + ns.write(const.SsoVersion) + ns.write(const.ServiceId) + ns.write(const.ClientVersion) + ns.write_hex("00 00") + + ns.write_tlv("00 19", ns.read_all()) + packet.body.write_func(tlv_019) + + def tlv_114(ns: utils.Stream): + ns.write(const.EcdhVersion) + ns.write_token(stru.ecdh.public_key) + + ns.write_tlv("01 14", ns.read_all()) + packet.body.write_func(tlv_114) + + packet.body.write_hex("03 05 00 1E 00 00 00 00 00 00 00 05 00 00 00 04 00") + packet.body.write_hex("00 00 00 00 00 00 48 00 00 00 02 00 00 00 02 00 00") + packet.body.write_hex("00 15 00 30 00 01 01 45 AA 72 E9 00 10") + packet.body.write_hex("6B 10 A0 47 00 00 00 00 00 00 00 00 00") + packet.body.write_hex("00 00 00 02 9A 76 CB 8F 00 10 D4 61 6E") + packet.body.write_hex("EB D5 B6 4E 2C 5B 6C FA C3 E0 FD 53 90") + + return packet diff --git a/core/protocol/wtlogin/p2_0818_qrcode/unpack.py b/core/protocol/wtlogin/p2_0818_qrcode/unpack.py new file mode 100644 index 0000000..722952e --- /dev/null +++ b/core/protocol/wtlogin/p2_0818_qrcode/unpack.py @@ -0,0 +1,32 @@ +from functools import partial + +from logger import logger +from core.utils import QRCode +from core.entities import QQStruct, Packet, PacketHandler + + +def check_0818(packet: Packet): + return packet.cmd == "08 18" + + +async def handle_0818(stru: QQStruct, packet: Packet): + if packet.body.read_byte() != 0x00: + logger.fatal("登录二维码获取失败,请尝试重新运行") + raise RuntimeError("Can't get login qrcode") + + stru.pckey_for_0819 = packet.body.del_left(6).read(16) + stru.token_0038_from_0818 = packet.body.del_left(4).read_token(True) + stru.token_by_scancode = packet.body.del_left(4).read_token(True) + + packet.body.del_left(4) + qrcode = QRCode(stru.path, packet.body.read_token()) + logger.info('登录二维码获取成功,已保存至' + qrcode.path) + qrcode.auto_show() + + +def unpack_0818(stru: QQStruct): + return PacketHandler( + temp=True, + check=check_0818, + handle=partial(handle_0818, stru) + ) diff --git a/core/protocol/wtlogin/p3_0819_scan/__init__.py b/core/protocol/wtlogin/p3_0819_scan/__init__.py new file mode 100644 index 0000000..d6b0466 --- /dev/null +++ b/core/protocol/wtlogin/p3_0819_scan/__init__.py @@ -0,0 +1,2 @@ +from .pack import pack_0819 +from .unpack import unpack_0819 \ No newline at end of file diff --git a/core/protocol/wtlogin/p3_0819_scan/pack.py b/core/protocol/wtlogin/p3_0819_scan/pack.py new file mode 100644 index 0000000..03180d2 --- /dev/null +++ b/core/protocol/wtlogin/p3_0819_scan/pack.py @@ -0,0 +1,26 @@ +from core import utils +from core import const +from core.entities import Packet, QQStruct + + +def pack_0819(stru: QQStruct): + packet = Packet( + cmd="08 19", + uin=stru.uin, + tea_key=stru.pckey_for_0819 + ) + packet.version.write(const.StructVersion) + packet.version.write_hex("00 30").write_token(stru.token_0038_from_0818) + + def tlv_019(ns: utils.Stream): + ns.write_hex("00 01") + ns.write(const.SsoVersion) + ns.write(const.ServiceId) + ns.write(const.ClientVersion) + ns.write_hex("00 00") + + ns.write_tlv("00 19", ns.read_all()) + packet.body.write_func(tlv_019) + + packet.body.write_hex("03 01").write_token(stru.token_by_scancode) + return packet diff --git a/core/protocol/wtlogin/p3_0819_scan/unpack.py b/core/protocol/wtlogin/p3_0819_scan/unpack.py new file mode 100644 index 0000000..bb73000 --- /dev/null +++ b/core/protocol/wtlogin/p3_0819_scan/unpack.py @@ -0,0 +1,34 @@ +import os +from functools import partial + +from logger import logger +from core.entities import QQStruct, Packet, PacketHandler + + +def check_0819(packet: Packet): + return packet.cmd == "08 19" + + +async def handle_0819(stru: QQStruct, packet: Packet): + state = packet.body.read_byte() + + if state == 0x01: + logger.info(f'账号 {packet.uin} 已扫码,请在手机上确认登录') + elif state == 0x00: + stru.uin = packet.uin + stru.password = packet.body.del_left(2).read_token() + stru.tgt_key = packet.body.del_left(2).read_token() + + path = os.path.join(stru.path, "QrCode.jpg") + if os.path.exists(path): + os.remove(path) + + logger.info(f'账号 {packet.uin} 已确认登录, 尝试登录中......') + + +def unpack_0819(stru: QQStruct): + return PacketHandler( + temp=True, + check=check_0819, + handle=partial(handle_0819, stru) + ) diff --git a/core/protocol/wtlogin/p4_0836_login/__init__.py b/core/protocol/wtlogin/p4_0836_login/__init__.py new file mode 100644 index 0000000..9f2d969 --- /dev/null +++ b/core/protocol/wtlogin/p4_0836_login/__init__.py @@ -0,0 +1,2 @@ +from .pack import pack_0836 +from .unpack import unpack_0836 \ No newline at end of file diff --git a/core/protocol/wtlogin/p4_0836_login/pack.py b/core/protocol/wtlogin/p4_0836_login/pack.py new file mode 100644 index 0000000..e0f3eff --- /dev/null +++ b/core/protocol/wtlogin/p4_0836_login/pack.py @@ -0,0 +1,164 @@ +from time import time +from binascii import crc32 + +from core import utils, croto +from logger import logger +from core import const +from core.entities import Packet, QQStruct + +private_0836key = const.RandKey +bufOfficiaKey = utils.rand_bytes(16) +OfficialData = utils.rand_bytes(56) + +def pack_0836(stru: QQStruct): + packet = Packet(cmd="08 36", tea_key=stru.ecdh.share_key, uin=stru.uin) + packet.version.write(const.StructVersion).write_hex("00 02") + packet.version.write(const.EcdhVersion) + packet.version.write_token(stru.ecdh.public_key).write_hex("00 00") + packet.version.write_token(private_0836key) + + computer_id = croto.md5(f"{stru.uin}ComputerID".encode()) + computer_id_ex = croto.md5(f"{stru.uin}ComputerIDEx".encode()) + private_bufMacGuid = croto.md5(f"{stru.uin}bufMacGuid".encode()) + + def tlv_112(ns: utils.Stream): + ns.write_tlv("01 12", stru.token_0038_from_0825) + packet.body.write_func(tlv_112) + + def tlv_30f(ns: utils.Stream): + ns.write_token(b'PY-PCQQ') + ns.write_tlv("03 0F", ns.read_all()) + packet.body.write_func(tlv_30f) + + def tlv_005(ns: utils.Stream): + ns.write_hex("00 02") + ns.write_int32(stru.uin) + + ns.write_tlv("00 05", ns.read_all()) + packet.body.write_func(tlv_005) + + if stru.is_scancode: + def tlv_303(ns: utils.Stream): + ns.write_tlv("03 03", stru.password) + packet.body.write_func(tlv_303) + + private_bufOfficial = croto.create_official(bufOfficiaKey, OfficialData, stru.password[2:]) + else: + logger.fatal("账密登录待开发, 请优先使用扫码登录") + raise RuntimeError("Can't login by password") + + def tlv_015(ns: utils.Stream): + computer_id2 = computer_id_ex[0:4] + bytes(12) + + ns.write_hex("00 00 01") + + ns.write_int32(crc32(computer_id2)) + ns.write_int16(16) + ns.write(computer_id2) + ns.write_byte(2) + + ns.write_int32(crc32(computer_id)) + ns.write_int16(16) + ns.write(computer_id) + + ns.write_tlv("00 15", ns.read_all()) + packet.body.write_func(tlv_015) + + def tlv_01a(ns: utils.Stream): + ns.write_func(tlv_015) + ns.write_tlv("00 1A", croto.tea_encrypt(ns.read_all(), stru.tgt_key)) + packet.body.write_func(tlv_01a) + + def tlv_018(ns: utils.Stream): + ns.write_hex("00 01") + ns.write(const.SsoVersion) + ns.write(const.ServiceId) + ns.write(const.ClientVersion) + ns.write_int32(stru.uin) + ns.write_int16(stru.redirection_times) + ns.write_hex("00 00") + + ns.write_tlv("00 18", ns.read_all()) + packet.body.write_func(tlv_018) + + def tlv_103(ns: utils.Stream): + ns.write_hex("00 01") + ns.write_hex("00 10") + ns.write(private_bufMacGuid) + + ns.write_tlv("01 03", ns.read_all()) + packet.body.write_func(tlv_103) + + def tlv_312(ns: utils.Stream): + ns.write_tlv("03 12", bytes.fromhex("01 00 00 00 00")) + packet.body.write_func(tlv_312) + + def tlv_508(ns: utils.Stream): + ns.write_hex("01 00 00 00") + ns.write_byte(2) + + ns.write_tlv("05 08", ns.read_all()) + packet.body.write_func(tlv_508) + + def tlv_313(ns: utils.Stream): + ns.write_hex("01 01 02") + ns.write_hex("00 10") + ns.write(private_bufMacGuid) + ns.write_hex("00 00 00 10") + + ns.write_tlv("03 13", ns.read_all()) + packet.body.write_func(tlv_313) + + def tlv_102(ns: utils.Stream): + ns.write_hex("00 91") + ns.write(bufOfficiaKey) + + ns.write_hex("00 38") + ns.write(OfficialData) + + ns.write_hex("00 14") + ns.write(private_bufOfficial) + ns.write_int32(crc32(private_bufOfficial)) + + ns.write_tlv("01 02", ns.read_all()) + packet.body.write_func(tlv_102) + + def tlv_501(ns: utils.Stream): + vFix = bytes([67, 70, 57, 109, 89, 84, 77, 114, 109, 49, + 75, 85, 48, 109, 121, 101, 54, 52, 80, 118]) + v2 = 872529248 # int((time.time()-psutil.boot_time()) * 1000) + + v13 = bytearray() + v1 = croto.rand_str2(v13) + v3 = int(time()).to_bytes(8, 'little') + v1 + computer_id + v4 = croto.sha1024(v3, bytes(v13)) + tlv0551_key = croto.md5(v4) + + ns.write_int32(stru.uin) + ns.write(const.SsoVersion) + ns.write_int32(v2) + ns.write_hex("00 00 08 36") + ns.write_token(private_bufMacGuid) + enc = ns.read_all() + + byte10 = bytearray() + v23 = croto.sub_16F90(enc, byte10, vFix, v13) + + ns.write(const.SsoVersion) + ns.write_int32(65536) + ns.write_byte(109) + ns.write_int32((1 | ((0 | (0 << 8)) << 8)) << 8) + ns.write_int32((2 | ((0 | (0 << 8)) << 8)) << 8) + ns.write_int32((2 | ((0 | (0 << 8)) << 8)) << 8) + ns.write(bytes(byte10)) + ns.write_int32(v2) + ns.write_int32(4) + ns.write_byte(20) + ns.write(v23) + ns.write(tlv0551_key) + ns.write_token(b'5151') + + ns.write_tlv("05 51", ns.read_all()) + packet.body.write_func(tlv_501) + + return packet \ No newline at end of file diff --git a/core/protocol/wtlogin/p4_0836_login/unpack.py b/core/protocol/wtlogin/p4_0836_login/unpack.py new file mode 100644 index 0000000..00ab56c --- /dev/null +++ b/core/protocol/wtlogin/p4_0836_login/unpack.py @@ -0,0 +1,69 @@ +from functools import partial + +from core import utils, croto +from logger import logger +from core import const +from core.entities import QQStruct, Packet, PacketHandler + + +def check_0836(packet: Packet): + return packet.cmd == "08 36" + + +async def handle_0836(stru: QQStruct, packet: Packet): + if packet.body.read_hex(2) != "01 03": + raise ValueError("08 36 Response Protocol Packet error") + + tk_key = packet.body.read_token() + twice_key = stru.ecdh.twice(tk_key) + raw_data = croto.tea_decrypt(packet.body.read_all(), twice_key) + + raw_data = croto.tea_decrypt(raw_data, stru.tgt_key) + if raw_data == b'': + raw_data = croto.tea_decrypt(raw_data, const.RandKey) + stream = utils.Stream(raw_data) + + sign = stream.read_byte() + if sign == 0x00: + # logger.info(f"登录状态校验 -> OK") + pass + elif sign == 0x01: + logger.fatal("登录状态校验 -> 需要更新TGTGT或需要二次解密") + exit(1) + elif sign == 0x33: + logger.fatal("登录状态校验 -> 当前上网环境异常") + exit(1) + elif sign == 0x34: + logger.fatal("登录状态校验 -> 需要验证密保或开启了设备锁") + exit(1) + else: + logger.fatal("登录状态校验 -> 发生未处理错误") + exit(1) + + while len(stream._raw) > 0: + cmd = stream.read_hex(2) + ns = utils.Stream(stream.read_token()) + + if cmd == "01 09": + stru.pckey_for_0828_send = ns.del_left(2).read(16) + stru.token_0038_from_0836 = ns.read_token() + + elif cmd == "01 07": + ns.read_int16() + ns.read_token() + + stru.pckey_for_0828_recv = ns.read(16) + stru.token_0088_from_0836 = ns.read_token() + + elif cmd == "01 08": + ns.read(8) + length = ns.read_byte() + stru.nickname = ns.read(length).decode() + + +def unpack_0836(stru: QQStruct): + return PacketHandler( + temp=True, + check=check_0836, + handle=partial(handle_0836, stru) + ) diff --git a/core/protocol/wtlogin/p5_0828_session/__init__.py b/core/protocol/wtlogin/p5_0828_session/__init__.py new file mode 100644 index 0000000..4155bce --- /dev/null +++ b/core/protocol/wtlogin/p5_0828_session/__init__.py @@ -0,0 +1,2 @@ +from .pack import pack_0828 +from .unpack import unpack_0828 \ No newline at end of file diff --git a/core/protocol/wtlogin/p5_0828_session/pack.py b/core/protocol/wtlogin/p5_0828_session/pack.py new file mode 100644 index 0000000..6b1496d --- /dev/null +++ b/core/protocol/wtlogin/p5_0828_session/pack.py @@ -0,0 +1,135 @@ +from binascii import crc32 + +from core import utils, croto +from core import const +from core.entities import Packet, QQStruct + + +def pack_0828(stru: QQStruct): + packet = Packet( + cmd="08 28", + uin=stru.uin, + tea_key=stru.pckey_for_0828_send + ) + packet.version.write(const.BodyVersion).write_hex("00 30 00 3A 00 38") + packet.version.write(stru.token_0038_from_0836) + + def tlv_007(ns: utils.Stream): + ns.write_tlv("00 07", stru.token_0088_from_0836) + packet.body.write_func(tlv_007) + + def tlv_00c(ns: utils.Stream): + ns.write_hex("00 02 00 00 00 00 00 00 00 00 00 00") + ns.write(stru.server_ip) + ns.write_int16(stru.addr[1]) + ns.write_hex("00 00 00 00") + + ns.write_tlv("00 0C", ns.read_all()) + packet.body.write_func(tlv_00c) + + def tlv_015(ns: utils.Stream): + computer_id = croto.md5(f"{stru.uin}ComputerID".encode()) + computer_id2 = croto.md5(f"{stru.uin}ComputerIDEx".encode()) + + ns.write_hex("00 00 01") + + ns.write_int32(crc32(computer_id2)) + ns.write_int16(16) + ns.write(computer_id2) + ns.write_byte(2) + + ns.write_int32(crc32(computer_id)) + ns.write_int16(16) + ns.write(computer_id) + + ns.write_tlv("00 15", ns.read_all()) + packet.body.write_func(tlv_015) + + def tlv_036(ns: utils.Stream): + ns.write_hex("00 02") + ns.write_hex("00 01") + ns.write(bytes(14)) + + ns.write_tlv("00 36", ns.read_all()) + packet.body.write_func(tlv_036) + + def tlv_018(ns: utils.Stream): + ns.write_hex("00 01") + ns.write(const.SsoVersion) + ns.write(const.ServiceId) + ns.write(const.ClientVersion) + ns.write_int32(stru.uin) + ns.write_int16(stru.redirection_times) + ns.write_hex("00 00") + + ns.write_tlv("00 18", ns.read_all()) + packet.body.write_func(tlv_018) + + def tlv_01f(ns: utils.Stream): + ns.write_hex("00 01") + ns.write(const.DeviceID) + + ns.write_tlv("00 1F", ns.read_all()) + packet.body.write_func(tlv_01f) + + def tlv_105(ns: utils.Stream): + ns.write_int16(1) + ns.write_hex("01 02") + + ns.write_int16(20) + ns.write_hex("01 01") + ns.write_int16(16) + ns.write(utils.rand_bytes(16)) + + ns.write_int16(20) + ns.write_hex("01 02") + ns.write_int16(16) + ns.write(utils.rand_bytes(16)) + + ns.write_tlv("01 05", ns.read_all()) + packet.body.write_func(tlv_105) + + def tlv_10b(ns: utils.Stream): + ns.write_hex("00 02") + ns.write(utils.rand_bytes(16)) # ClientMD5 服务端不作校验 + ns.write(utils.rand_bytes(1)) # QDFlag 服务端不作校验 + ns.write_hex("10 00 00 00 00 00 00 00 02") + + # QDDATA 服务端不作校验 + + ns.write_hex("00 63") + ns.write_hex("3E 00 63 02") + ns.write(const.DWQDVersion) + ns.write_hex("00 04 00") + ns.write(utils.rand_bytes(2)) + ns.write_hex("00 00 00 00") + ns.write(utils.rand_bytes(16)) + + ns.write_hex("01 00") + ns.write_hex("00 00 00 00") + ns.write_hex("00 00") + ns.write_hex("00 00 00 00 00 00 00 00") + + ns.write_hex("00 00 00 01") + ns.write_hex("00 00 00 01") + ns.write_hex("00 00 00 01") + ns.write_hex("00 00 00 01") + ns.write_hex("00 FE 26 81 75 EC 2A 34 EF") + + ns.write_hex("02 3E 50 39 6D B1 AF CC 9F EA 54 E1") + ns.write_hex("70 CC 6C 9E 4E 63 8B 51 EC 7C 84 5C") + + ns.write_hex("68") + ns.write_hex("00 00 00 00") + + ns.write_tlv("01 0B", ns.read_all()) + packet.body.write_func(tlv_10b) + + def tlv_20d(ns: utils.Stream): + ns.write_hex("00 01") + ns.write(stru.local_ip) + + ns.write_tlv("02 0D", ns.read_all()) + packet.body.write_func(tlv_20d) + + return packet diff --git a/core/protocol/wtlogin/p5_0828_session/unpack.py b/core/protocol/wtlogin/p5_0828_session/unpack.py new file mode 100644 index 0000000..63c4723 --- /dev/null +++ b/core/protocol/wtlogin/p5_0828_session/unpack.py @@ -0,0 +1,34 @@ +from functools import partial + +from logger import logger +from core.utils import Stream +from core.entities import QQStruct, Packet, PacketHandler + + +def check_0828(packet: Packet): + return packet.cmd == "08 28" + + +async def handle_0828(stru: QQStruct, packet: Packet): + sign = packet.body.read_byte() + if sign == 0x00: + #logger.info("会话密匙申请 -> OK") + pass + else: + logger.fatal("会话密匙申请 -> 发生未处理错误") + exit(1) + + while len(packet.body._raw) > 0: + cmd = packet.body.read_hex(2) + ns = Stream(packet.body.read_token()) + + if cmd == "01 0C": + stru.session_key = ns.del_left(2).read(16) + + +def unpack_0828(stru: QQStruct): + return PacketHandler( + temp=True, + check=check_0828, + handle=partial(handle_0828, stru) + ) diff --git a/core/protocol/wtlogin/p6_00ec_online/__init__.py b/core/protocol/wtlogin/p6_00ec_online/__init__.py new file mode 100644 index 0000000..fc6098b --- /dev/null +++ b/core/protocol/wtlogin/p6_00ec_online/__init__.py @@ -0,0 +1,2 @@ +from .pack import pack_00ec +from .unpack import unpack_00ec \ No newline at end of file diff --git a/core/protocol/wtlogin/p6_00ec_online/pack.py b/core/protocol/wtlogin/p6_00ec_online/pack.py new file mode 100644 index 0000000..aa98c57 --- /dev/null +++ b/core/protocol/wtlogin/p6_00ec_online/pack.py @@ -0,0 +1,20 @@ +from core import const +from core.entities import Packet, QQStruct + + +def pack_00ec(stru: QQStruct): + packet = Packet( + cmd="00 EC", + uin=stru.uin, + tea_key=stru.session_key + ) + packet.version.write(const.BodyVersion) + + packet.body.write_hex("01 00") + packet.body.write_byte(const.StateOnline) # 上线 + packet.body.write_hex("00 01") + packet.body.write_hex("00 01") + packet.body.write_hex("00 04") + packet.body.write_hex("00 00 00 00") + + return packet \ No newline at end of file diff --git a/core/protocol/wtlogin/p6_00ec_online/unpack.py b/core/protocol/wtlogin/p6_00ec_online/unpack.py new file mode 100644 index 0000000..380b8be --- /dev/null +++ b/core/protocol/wtlogin/p6_00ec_online/unpack.py @@ -0,0 +1,25 @@ +from functools import partial + +from logger import logger +from core.entities import Packet, PacketHandler, QQStruct + + +def check_00ec(packet: Packet): + return packet.cmd == "00 EC" + + +async def handle_00ec(stru: QQStruct, packet: Packet): + stru.is_running = True + + if stru.password != b'': + logger.info(f"扫码登录成功, 欢迎尊敬的用户 {stru.nickname}({stru.uin}) 使用本协议库") + else: + logger.info(f"Token重登成功, 欢迎尊敬的用户 {stru.nickname}({stru.uin}) 使用本协议库") + + +def unpack_00ec(stru: QQStruct): + return PacketHandler( + temp=True, + check=check_00ec, + handle=partial(handle_00ec, stru) + ) diff --git a/core/protocol/wtlogin/p7_001d_skey/__init__.py b/core/protocol/wtlogin/p7_001d_skey/__init__.py new file mode 100644 index 0000000..f7eeda7 --- /dev/null +++ b/core/protocol/wtlogin/p7_001d_skey/__init__.py @@ -0,0 +1,2 @@ +from .pack import pack_001d +from .unpack import unpack_001d \ No newline at end of file diff --git a/core/protocol/wtlogin/p7_001d_skey/pack.py b/core/protocol/wtlogin/p7_001d_skey/pack.py new file mode 100644 index 0000000..5ecef80 --- /dev/null +++ b/core/protocol/wtlogin/p7_001d_skey/pack.py @@ -0,0 +1,22 @@ +from core import const +from core.entities import Packet, QQStruct + + +def pack_001d(stru: QQStruct): + packet = Packet( + cmd="00 1D", + uin=stru.uin, + tea_key=stru.session_key + ) + packet.version.write(const.BodyVersion) + + packet.body.write_byte(51) + packet.body.write_int16(6) # 域名数量 + packet.body.write_token(b't.qq.com') + packet.body.write_token(b'qun.qq.com') + packet.body.write_token(b'qzone.qq.com') + packet.body.write_token(b'jubao.qq.com') + packet.body.write_token(b'ke.qq.com') + packet.body.write_token(b'tenpay.com') + + return packet \ No newline at end of file diff --git a/core/protocol/wtlogin/p7_001d_skey/unpack.py b/core/protocol/wtlogin/p7_001d_skey/unpack.py new file mode 100644 index 0000000..3e5ae13 --- /dev/null +++ b/core/protocol/wtlogin/p7_001d_skey/unpack.py @@ -0,0 +1,28 @@ +from functools import partial +from core.entities import Packet, PacketHandler, QQStruct + + +def check_001d(packet: Packet): + return packet.cmd == "00 1D" + + +async def handle_001d(stru: QQStruct, packet: Packet): + packet.body.read(2) + stru.skey = packet.body.read_token().decode() + stru.bkn = gtk_skey(stru.skey) + stru.cookie = "uin=o%d; skey=%s" % (stru.uin, stru.skey) + + +def unpack_001d(stru: QQStruct): + return PacketHandler( + temp=True, + check=check_001d, + handle=partial(handle_001d, stru) + ) + + +def gtk_skey(skey: str) -> int: + accu = 5381 + for s in skey: + accu += (accu << 5) + ord(s) + return accu & 2147483647 diff --git a/core/protocol/wtlogin/p8_0058_heartbeat/__init__.py b/core/protocol/wtlogin/p8_0058_heartbeat/__init__.py new file mode 100644 index 0000000..9dfa43c --- /dev/null +++ b/core/protocol/wtlogin/p8_0058_heartbeat/__init__.py @@ -0,0 +1,2 @@ +from .pack import pack_0058 +from .unpack import unpack_0058 \ No newline at end of file diff --git a/core/protocol/wtlogin/p8_0058_heartbeat/pack.py b/core/protocol/wtlogin/p8_0058_heartbeat/pack.py new file mode 100644 index 0000000..8b81d09 --- /dev/null +++ b/core/protocol/wtlogin/p8_0058_heartbeat/pack.py @@ -0,0 +1,15 @@ +from core import const +from core.entities import Packet, QQStruct + + +def pack_0058(stru: QQStruct): + packet = Packet( + cmd="00 58", + uin=stru.uin, + tea_key=stru.session_key + ) + + packet.version.write(const.BodyVersion) + packet.body.write_hex("00 01 00 01") + + return packet diff --git a/core/protocol/wtlogin/p8_0058_heartbeat/unpack.py b/core/protocol/wtlogin/p8_0058_heartbeat/unpack.py new file mode 100644 index 0000000..98854f7 --- /dev/null +++ b/core/protocol/wtlogin/p8_0058_heartbeat/unpack.py @@ -0,0 +1,26 @@ +from logger import logger +from core.entities import Packet, PacketHandler, QQStruct + + +def check_0058(packet: Packet): + return packet.cmd == "00 58" + + +async def handle_0058(packet: Packet): + sign = packet.body.read_byte() + + if sign == 0x00: + # logger.info("心跳正常") + pass + elif sign == 0x01: + logger.error("当前客户端掉线, 请刷新在线状态") + elif sign == 0x17: + logger.error("当前客户端掉线, 请重新登录") + + +def unpack_0058(stru: QQStruct): + return PacketHandler( + temp=False, + check=check_0058, + handle=handle_0058 + ) diff --git a/core/protocol/wtlogin/p9_0062_login_out/__init__.py b/core/protocol/wtlogin/p9_0062_login_out/__init__.py new file mode 100644 index 0000000..9631dec --- /dev/null +++ b/core/protocol/wtlogin/p9_0062_login_out/__init__.py @@ -0,0 +1 @@ +from .pack import pack_0062 \ No newline at end of file diff --git a/core/protocol/wtlogin/p9_0062_login_out/pack.py b/core/protocol/wtlogin/p9_0062_login_out/pack.py new file mode 100644 index 0000000..343e329 --- /dev/null +++ b/core/protocol/wtlogin/p9_0062_login_out/pack.py @@ -0,0 +1,15 @@ +from core import const +from core.entities import Packet, QQStruct + + +def pack_0062(stru: QQStruct): + packet = Packet( + cmd="00 62", + uin=stru.uin, + tea_key=stru.session_key + ) + packet.version.write(const.BodyVersion) + + packet.body.write(bytes(16)) + stru.is_running = False + return packet diff --git a/core/protocol/wtlogin/wtlogin.py b/core/protocol/wtlogin/wtlogin.py new file mode 100644 index 0000000..e560131 --- /dev/null +++ b/core/protocol/wtlogin/wtlogin.py @@ -0,0 +1,144 @@ +import os +import sys +import asyncio + +from logger import logger +from core.utils import Stream +from core import const +from core.client import QQClient + +from .p1_0825_ping import pack_0825, unpack_0825 +from .p2_0818_qrcode import pack_0818, unpack_0818 +from .p3_0819_scan import pack_0819, unpack_0819 +from .p4_0836_login import pack_0836, unpack_0836 +from .p5_0828_session import pack_0828, unpack_0828 +from .p6_00ec_online import pack_00ec, unpack_00ec +from .p7_001d_skey import pack_001d, unpack_001d +from .p8_0058_heartbeat import pack_0058, unpack_0058 +from .p9_0062_login_out import pack_0062 + +def login_out(cli: QQClient): + if not cli.stru.is_running: + raise RuntimeError("当前客户端还未登录, 无法执行下线操作") + + cli.send(pack_0062(cli.stru)) + logger.info("已退出当前登录, 期待您的下次使用") + + +async def login_by_scan(cli: QQClient): + # ping 服务器 + cli.send(pack_0825(cli.stru)) + cli.manage.append(unpack_0825(cli.stru)) + await cli.recv_and_exec(const.RandKey) + + # 申请登录二维码 + cli.send(pack_0818(cli.stru)) + cli.manage.append(unpack_0818(cli.stru)) + await cli.recv_and_exec(cli.stru.ecdh.share_key) + + # 每两秒判断一次扫码结果 + while cli.stru.password == b'': + await asyncio.sleep(2) + + cli.send(pack_0819(cli.stru)) + cli.manage.append(unpack_0819(cli.stru)) + await cli.recv_and_exec(cli.stru.pckey_for_0819) + + # 进行登录校验 + cli.send(pack_0836(cli.stru)) + cli.manage.append(unpack_0836(cli.stru)) + await cli.recv_and_exec(cli.stru.ecdh.share_key) + + # 申请会话密匙 + cli.send(pack_0828(cli.stru)) + cli.manage.append(unpack_0828(cli.stru)) + await cli.recv_and_exec(cli.stru.pckey_for_0828_recv) + + # 申请网络密匙 + cli.send(pack_001d(cli.stru)) + cli.manage.append(unpack_001d(cli.stru)) + await cli.recv_and_exec(cli.stru.session_key) + + # 置上线状态 + cli.send(pack_00ec(cli.stru)) + cli.manage.append(unpack_00ec(cli.stru)) + await cli.recv_and_exec(cli.stru.session_key) + + # 开启心跳循环 + async def heart_beat(): + cli.send(pack_0058(cli.stru)) + await asyncio.sleep(40) + cli.add_task(heart_beat()) + cli.manage.append(unpack_0058(cli.stru)) + cli.add_task(heart_beat()) + + # 写入 Token 用于二次登录 + path = os.path.join(cli.stru.path, "session.token") + with open(path, "wb") as f: + stream = Stream() + + stream.write_token(cli.stru.server_ip) + stream.write_int32(cli.stru.uin) + stream.write_token(cli.stru.nickname.encode()) + + stream.write_token(cli.stru.token_0038_from_0836) + stream.write_token(cli.stru.token_0088_from_0836) + stream.write_token(cli.stru.pckey_for_0828_send) + stream.write_token(cli.stru.pckey_for_0828_recv) + f.write(stream.read_all()) + + + logger.info(f"已写入 Token 至 {path}, 下次登录无需再进行扫码") + +async def login_by_token(cli: QQClient): + # ping 服务器 + cli.send(pack_0825(cli.stru)) + cli.manage.append(unpack_0825(cli.stru)) + await cli.recv_and_exec(const.RandKey) + + # 读取 Token 用于二次登录 + path = os.path.join(cli.stru.path, "session.token") + if not os.path.exists(path): + logger.error(f"{path} 所指向的 Token 文件不存在, 请先使用扫码登录生成") + sys.exit(0) + + with open(path, "rb") as f: + stream = Stream(f.read()) + + cli.stru.server_ip = stream.read_token() + cli.stru.uin = stream.read_int32() + cli.stru.nickname = stream.read_token().decode() + cli.stru.token_0038_from_0836 = stream.read_token() + cli.stru.token_0088_from_0836 = stream.read_token() + cli.stru.pckey_for_0828_send = stream.read_token() + cli.stru.pckey_for_0828_recv = stream.read_token() + + try: + # 申请会话密匙 + cli.send(pack_0828(cli.stru)) + cli.manage.append(unpack_0828(cli.stru)) + + coro = cli.recv_and_exec(cli.stru.pckey_for_0828_recv) + await asyncio.wait_for(coro, timeout=10) + except: + os.remove(path) + logger.error(f"本地Token已失效, 请重新运行程序使用扫码登录") + sys.exit(0) + + # 置上线状态 + cli.send(pack_001d(cli.stru)) + cli.manage.append(unpack_001d(cli.stru)) + await cli.recv_and_exec(cli.stru.session_key) + + # 置上线状态 + cli.send(pack_00ec(cli.stru)) + cli.manage.append(unpack_00ec(cli.stru)) + await cli.recv_and_exec(cli.stru.session_key) + + # 开启心跳循环 + async def heart_beat(): + cli.send(pack_0058(cli.stru)) + await asyncio.sleep(40) + cli.add_task(heart_beat()) + cli.manage.append(unpack_0058(cli.stru)) + cli.add_task(heart_beat()) \ No newline at end of file diff --git a/core/utils/__init__.py b/core/utils/__init__.py new file mode 100644 index 0000000..d3c5dd7 --- /dev/null +++ b/core/utils/__init__.py @@ -0,0 +1,11 @@ +from .rand import ( + rand_bytes, + rand_tcp_host, + rand_udp_host +) + +from .qrcode import QRCode + +from .stream import Stream + +from .socket import AsyncTCPSocket, UDPSocket diff --git a/core/utils/qrcode.py b/core/utils/qrcode.py new file mode 100644 index 0000000..7d7f7b8 --- /dev/null +++ b/core/utils/qrcode.py @@ -0,0 +1,90 @@ +import os +import platform +import subprocess + + +class QRCode: + def __init__(self, path: str, data: bytes, qr_len=37) -> None: + self.path = os.path.join(path, "QrCode.jpg") + with open(self.path, mode="wb") as f: + f.write(data) + + self.qr_len = qr_len + + def open_in_windows(self): + return os.startfile(self.path) + + def open_in_linux(self): + return subprocess.call(["xdg-open", self.path]) + + def open_in_macos(self): + return subprocess.call(["open", self.path]) + + def print_in_terminal(self): + from PIL import Image + + im = Image.open(self.path) + im = im.resize((self.qr_len, self.qr_len), Image.NEAREST) + + txt = '' + for i in range(self.qr_len): + for j in range(self.qr_len): + content = im.getpixel((j, i)) + if isinstance(content, int): + content = (content, content, content) + txt += rgb_to_char(*content) + txt += '\n' + im.close() + print(make_qrtex(txt)) + + def auto_show(self): + system = platform.system() + try: + # 用系统默认程序打开图片文件 + if system == "Windows": + self.open_in_windows() + elif system == "Linux": + self.open_in_linux() + elif system == "Darwin": + self.open_in_macos() + else: + raise SystemError("This device system is unknown") + except: + # 使用pillow库在终端上打印二维码图片 + self.print_in_terminal() + + +def rgb_to_char(r, g, b, alpha=256): + if alpha == 0: + return ' ' + gary = (2126 * r + 7152 * g + 722 * b) / 10000 + ascii_char = list("■□") + # gary / 256 = x / len(ascii_char) + x = int(gary / (alpha + 1.0) * len(ascii_char)) + return ascii_char[x] + + +def make_qrtex(QR_Tab): + tmp_text = "" + print_tex = "" + + atrr = 7 + fore = 37 + back = 47 + color_block = "\x1B[%d;%d;%dm" % (atrr, fore, back) + atrr = 0 + fore = 0 + back = 0 + color_none = "\x1B[%d;%d;%dm" % (atrr, fore, back) + + for loop in range(0, len(QR_Tab)): + if QR_Tab[loop] == '■': + tmp_text = "%s \x1B[0m" % (color_block) + elif QR_Tab[loop] == '□': + tmp_text = "%s \x1B[0m" % (color_none) + else: + tmp_text = "\n" + + print_tex = print_tex + tmp_text + + return print_tex diff --git a/core/utils/rand.py b/core/utils/rand.py new file mode 100644 index 0000000..4a6bb5b --- /dev/null +++ b/core/utils/rand.py @@ -0,0 +1,33 @@ +import random +import socket + + +def rand_bytes(size: int): + return bytes([random.randint(0, 255) for _ in range(size)]) + + +def rand_tcp_host(): + host = random.choice([ + "tcpconn.tencent.com", + "tcpconn2.tencent.com", + "tcpconn3.tencent.com", + "tcpconn4.tencent.com" + ]) + + return socket.gethostbyname(host) + + +def rand_udp_host(): + host = random.choice([ + "sz.tencent.com", + "sz2.tencent.com", + "sz3.tencent.com", + "sz4.tencent.com", + "sz5.tencent.com", + "sz6.tencent.com", + "sz7.tencent.com", + "sz8.tencent.com", + "sz9.tencent.com", + ]) + + return socket.gethostbyname(host) \ No newline at end of file diff --git a/core/utils/socket.py b/core/utils/socket.py new file mode 100644 index 0000000..d6aa6dd --- /dev/null +++ b/core/utils/socket.py @@ -0,0 +1,48 @@ +import socket +import asyncio + + +class AsyncTCPSocket: + def __init__(self, host, port, loop): + self.host = host + self.port = port + self.loop = loop + self.lock = asyncio.Lock(loop=loop) + + async def init(self): + reader, writer = await asyncio.open_connection( + host=self.host, + port=self.port, + loop=self.loop + ) + + self.reader = reader + self.writer = writer + + def send(self, data: bytes): + size = len(data) + 2 + self.writer.write(size.to_bytes(2, "big") + data) + + async def recv(self): + async with self.lock: + temp = await self.reader.read(2) + size = int.from_bytes(temp, "big") + + data = await self.reader.read(size) + return data + + +class UDPSocket: + def __init__(self, host: str, port: int): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sock.settimeout(60) + + self.host = socket.gethostbyname(host) + self.port = port + + def send(self, data: bytes): + return self.sock.sendto(data, (self.host, self.port)) + + def recv(self): + data, _ = self.sock.recvfrom(1024 * 10) + return data diff --git a/core/utils/stream.py b/core/utils/stream.py new file mode 100644 index 0000000..1243e76 --- /dev/null +++ b/core/utils/stream.py @@ -0,0 +1,107 @@ +import re + + +class Stream: + def __init__(self, raw=bytes(0)): + self._raw = raw + + def __str__(self): + if self._raw == None: + self._raw = b'' + + str = re.findall(".{2}", self._raw.hex().upper()) + return f"Stream({' '.join(str)})" + + def __repr__(self): + if self._raw == None: + self._raw = b'' + return f"Stream(Size: {len(self._raw)})" + + def write(self, bin: bytes): + self._raw += bin + return self + + def write_int16(self, num: int): + return self.write(num.to_bytes(2, 'big')) + + def write_int32(self, num: int): + return self.write(num.to_bytes(4, 'big')) + + def write_varint(self, num: int): + val = bytearray() + while num > 127: + val.append(0x80 | num & 0x7F) + num = num >> 7 + val.append(num) + return self.write(bytes(val)) + + def write_pbint(self, num: int, mark: int): + mark = 8 * mark + self.write_varint(mark) + self.write_varint(num) + + def write_pbdata(self, data: bytes, mark: int): + mark = 2 + 8 * mark + self.write_varint(mark) + self.write_byte(len(data)) + self.write(data) + + def write_pbhex(self, hex: str, mark: int): + return self.write_pbdata(bytes.fromhex(hex), mark) + + def write_byte(self, byte: int): + return self.write(byte.to_bytes(1, 'big')) + + def write_list(self, num_list: list): + return self.write(bytes(num_list)) + + def write_hex(self, hex: str): + return self.write(bytes.fromhex(hex)) + + def write_token(self, token: bytes): + return self.write_int16(len(token)).write(token) + + def write_tlv(self, cmd: str, bin: bytes): + return self.write_hex(cmd).write_token(bin) + + def write_stream(self, new_stream): + return self.write(new_stream.read_all()) + + def write_func(self, func): + new_stream = Stream(b'') + func(new_stream) + return self.write_stream(new_stream) + + def del_left(self, length: int): + self._raw = self._raw[length:] + return self + + def del_right(self, length: int): + self._raw = self._raw[:-length] + return self + + def read(self, size: int): + bin = self._raw[0:size] + self._raw = self._raw[size:] + return bin + + def read_all(self): + return self.read(len(self._raw)) + + def read_byte(self): + return self.read(1)[0] + + def read_int16(self): + return int.from_bytes(self.read(2), 'big') + + def read_int32(self): + return int.from_bytes(self.read(4), 'big') + + def read_hex(self, size: int): + string = self.read(size).hex().upper() + return " ".join(re.findall(".{2}", string)) + + def read_token(self, keep_size=False): + size = self.read_int16() + data = self.read(size) + return size.to_bytes(2, 'big') + data if keep_size else data \ No newline at end of file diff --git a/default/__init__.py b/default/__init__.py new file mode 100644 index 0000000..cb8a14e --- /dev/null +++ b/default/__init__.py @@ -0,0 +1,11 @@ +from .bot import run_bot +from .plugin import ( + Session, + only_group, + only_private, + check_user, + check_session, + on_event, + on_command, + on_message +) \ No newline at end of file diff --git a/default/bot.py b/default/bot.py new file mode 100644 index 0000000..1f05a8a --- /dev/null +++ b/default/bot.py @@ -0,0 +1,37 @@ +import os + +from core.client import QQClient +from core.entities import MessageEvent +from core.protocol import wtlogin, event +from .plugin import Session, default_manager + + +def run_bot(uin: int = 0): + cli = QQClient(uin) + + path = os.path.join(cli.stru.path, "session.token") + if os.path.exists(path): + cli.run_task(wtlogin.login_by_token(cli)) + else: + cli.run_task(wtlogin.login_by_scan(cli)) + + async def plugin_handle_message(event: MessageEvent): + session = Session( + driver=cli, + message=event.message.format(), + event=event, + matched=None + ) + + await default_manager.exec_all(session) + + cli.manage.append(event.handle_msg_receipt(cli)) + cli.manage.append(event.handle_group_msg(cli, plugin_handle_message)) + cli.manage.append(event.handle_private_msg(cli, plugin_handle_message)) + + async def run_handler(): + await cli.recv_and_exec(cli.stru.session_key) + cli.add_task(run_handler()) + cli.add_task(run_handler()) + + cli.loop.run_forever() \ No newline at end of file diff --git a/default/plugin.py b/default/plugin.py new file mode 100644 index 0000000..233627d --- /dev/null +++ b/default/plugin.py @@ -0,0 +1,194 @@ +import traceback + +from asyncio import Queue +from dataclasses import dataclass +from typing import Any, List, Union, Iterable, Callable, Coroutine + +from logger import logger +from core.protocol import pcapi, webapi +from core.client import QQClient +from core.entities import ( + MessageEvent, + NoticeEvent, + Message, + TextNode +) + + +@dataclass +class Session: + driver: QQClient + message: str + event: Union[MessageEvent, NoticeEvent] + matched: Union[str, list] + + async def aget(self, prompt: str): + await self.send_msg(prompt) + chan = Queue(maxsize=1, loop=self.driver.loop) + + @on_event(check_session(self), temp=True, block=True) + async def wait_input(session: Session): + await chan.put(session.message) + + return await chan.get() + + async def send_msg(self, text: str): + if self.event.group_id != 0: + await self.send_group_msg(self.event.group_id, text) + elif self.event.user_id != 0: + await self.send_private_msg(self.event.user_id, text) + + async def send_group_msg(self, group_id: int, text: str): + node = TextNode(text) + pcapi.send_group_msg(self.driver, group_id, Message().add(node)) + + group = await webapi.get_group(self.driver, group_id) + logger.info(f"发送群聊 {group.name}({group_id}) 消息 -> {text}") + + async def send_private_msg(self, user_id: int, text: str): + node = TextNode(text) + pcapi.send_private_msg(self.driver, user_id, Message().add(node)) + + user = await webapi.get_user(self.driver, user_id) + logger.info(f"发送私聊 {user.name}({user_id}) 消息 -> {text}") + + async def set_group_card(self, group_id: int, user_id: int, new_card: str): + await webapi.set_group_card( + self.driver.stru, + group_id, + user_id, + new_card + ) + + +Rule = Callable[[Session], Coroutine[Any, Any, bool]] + + +@dataclass +class Plugin: + rules: Iterable[Rule] + temp: bool + block: bool + priority: int + handle: Callable[[Session], Coroutine[Any, Any, None]] + + async def exec(self, session: Session): + for rule in self.rules: + if await rule(session): + continue + return None + await self.handle(session) + + +class PluginManager(List[Plugin]): + def append(self, plugin: Plugin): + if not isinstance(plugin, Plugin): + raise ValueError("PluginManager 添加的元素不是 Plugin 类型") + + if not plugin in self: + super().append(plugin) + + return self + + def pop(self, plugin: Plugin): + if plugin in self: + super().pop(self.index(plugin)) + + return self + + async def exec_all(self, session: Session): + try: + for plugin in self: + await plugin.exec(session) + if plugin.temp: + self.pop(plugin) + if plugin.block: + continue + except: + traceback.print_exc() + +# =============== Buttlin Rule =============== + + +async def only_message(session: Session): + return isinstance(session.event, MessageEvent) + + +async def only_notice(session: Session): + return isinstance(session.event, NoticeEvent) + + +async def only_private(session: Session): + event = session.event + return event.group_id == 0 and event.user_id != 0 + + +async def only_group(session: Session): + return session.event.group_id != 0 + + +def check_user(*user_ids: int): + async def new_rule(session: Session): + return session.event.user_id in user_ids + return new_rule + + +def check_session(session: Session): + async def new_rule(next_session: Session): + event = session.event + next_event = next_session.event + + if event.user_id != next_event.user_id: + return False + + if event.group_id != next_event.group_id: + return False + + return True + + return new_rule + +# =============== Buttlin Plugin Registrar =============== + + +default_manager = PluginManager() + + +def on_event(*rules: Rule, temp=False, block=False, priority=10): + def decorator(handle: Callable[[Session], Coroutine[Any, Any, None]]): + default_manager.append(Plugin( + rules=rules, + temp=temp, + block=block, + priority=priority, + handle=handle + )) + + return decorator + + +def on_message(*rules: Rule, temp=False, block=False, priority=10): + return on_event( + only_message, + *rules, + temp=temp, + block=block, + priority=priority + ) + + +def on_command(cmd: str, *rules: Rule, temp=False, block=False, priority=10): + async def cmd_rule(session: Session): + text = session.message + if text.startswith(cmd): + session.matched = text.replace(cmd, "", 1).strip() + return True + return False + + return on_event( + cmd_rule, + *rules, + temp=temp, + block=block, + priority=priority + ) diff --git a/example.py b/example.py index 961dad9..39d8852 100644 --- a/example.py +++ b/example.py @@ -1,49 +1,19 @@ -import pcqq +""" +Created on Wed Mar 2 at 22:14 2023 +联系方式: +@QQ: 2224825532 +@Github: https://github.com/DawnNights -@pcqq.on(pcqq.check_type("group_increase", "group_decrease")) -async def Welcome(session: pcqq.Session): - if session.event_type == "group_increase": - await session.send_msg({"type": "at", "qq": session.target_id}, "欢迎新人") - else: - await session.send_msg( - {"type": "at", "qq": session.target_id}, - f"({session.target_id})退出了群聊" - ) +""" +import default as pcqq -@pcqq.on_full("报时") -async def NowTime(session: pcqq.Session): - await session.send_msg({"type": "at", "qq": session.user_id}, pcqq.utils.time_lapse(0)) - - -@pcqq.on_fulls(["来份涩图", "我要涩涩"]) -async def SetuTime(session: pcqq.Session): - ret = (await pcqq.client.webget( - url="https://api.lolicon.app/setu/v2?size=original&size=regular&proxy=i.pixiv.re" - )).json()["data"][0] - print("网络图片", ret["urls"]["regular"]) - await session.send_msg({"type": "image", "url": ret["urls"]["regular"]}, "\n".join([ - "Title: " + ret["title"], - "Author: " + ret["author"], - "Tags: " + ", ".join(ret["tags"]), - "ImgUrl: " + ret["urls"]["original"], - ])) - - -@pcqq.on_command("点歌", prompt="请发送要点的歌名") -async def QQMusic(session: pcqq.Session): - await session.send_msg({"type": "music", "keyword": session.matched}) - - -@pcqq.on_regex(r"^禁言\[PQ:at,qq=(\d{6,11})\] (\d{1,7})$") -async def ShutUp(session: pcqq.Session): - qq, secs = session.matched[0] - await pcqq.set_group_shutup(session.group_id, int(qq), int(secs)) - -@pcqq.on_regex(r"^改名\[PQ:at,qq=(\d{6,11})\] (.+?)$") -async def ShutUp(session: pcqq.Session): - qq, nickname = session.matched[0] - await pcqq.set_group_card(session.group_id, int(qq), nickname) +@pcqq.on_message() +async def speak(session: pcqq.Session): + if session.message == "复读": + sentence = await session.aget("请输入要复读的话") + await session.send_msg(sentence) pcqq.run_bot() +dict.setdefault \ No newline at end of file diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..cd1daeb --- /dev/null +++ b/logger.py @@ -0,0 +1,11 @@ +import logging + +__all__ = ['logger'] + +logger = logging.getLogger("PY-PCQQ") +logger.setLevel(logging.INFO) + +steram_handler = logging.StreamHandler() +steram_formatter = logging.Formatter("[%(name)s] %(message)s") +steram_handler.setFormatter(steram_formatter) +logger.addHandler(steram_handler) \ No newline at end of file diff --git a/pcqq/__init__.py b/pcqq/__init__.py deleted file mode 100644 index 8c0649f..0000000 --- a/pcqq/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -""" - -""" -from .bot import only_login, run_bot -from .network import ( - login_out, - set_group_card, - set_group_shutup, - send_friend_msg, - send_group_msg, - set_online, -) -from .plugin import ( - on, - on_full, - on_fulls, - on_keyword, - on_keywords, - on_command, - on_commands, - on_regex, - - only_group, - only_friend, - check_at, - check_type, - check_user, - check_group, - check_session, - must_given -) -from .session import Session diff --git a/pcqq/binary/__init__.py b/pcqq/binary/__init__.py deleted file mode 100644 index cfddcf6..0000000 --- a/pcqq/binary/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -from .qqtea import QQTea -from .reader import Reader -from .writer import Writer -from .qqtlv import ( - tlv112, - tlv30f, - tlv004, - tlv005, - tlv303, - tlv006, - tlv015, - tlv01a, - tlv018, - tlv019, - tlv305, - tlv114, - tlv103, - tlv312, - tlv508, - tlv313, - tlv102, - tlv309, - tlv036, - tlv007, - tlv00c, - tlv01f, - tlv105, - tlv10b, - tlv02d -) \ No newline at end of file diff --git a/pcqq/binary/qqtlv.py b/pcqq/binary/qqtlv.py deleted file mode 100644 index 9172b4f..0000000 --- a/pcqq/binary/qqtlv.py +++ /dev/null @@ -1,490 +0,0 @@ -import pcqq.const as const -import pcqq.utils as utils -import pcqq.binary as binary - - -def _tlv(cmd: str, bin: bytes) -> bytes: - writer = binary.Writer() - - writer.write_hex(cmd) - writer.write_int16(len(bin)) - writer.write(bin) - - return writer.clear() - - -def tlv004() -> bytes: - """ - TLV_0004_QRLogin - - """ - writer = binary.Writer() - - writer.write_hex("00 00") - writer.write_int16(8) - writer.write(b'qr_login') - - return _tlv("00 04", writer.clear()) - - -def tlv112(token: bytes) -> bytes: - """ - TLV_0112_SigIP2 - - :param token: Token 0038 From 0825 Protocol Package - - """ - writer = binary.Writer() - - writer.write(token) - - return _tlv("01 12", writer.clear()) - - -def tlv30f() -> bytes: - """ - TLV_030F_ComputerName - - """ - writer = binary.Writer() - - # ComputerName Length - writer.write_int16(15) - # ComputerName Data - writer.write_hex("44 61 77 6E 4E 69 67 68 74 73 2D 50 43 51 51") - - return _tlv("03 0F", writer.clear()) - - -def tlv005(uin: int) -> bytes: - """ - TLV_0005_Uin - - :param uin: QQ Number - - """ - writer = binary.Writer() - - writer.write_hex("00 02") # Tlv Version - writer.write_int32(uin) - - return _tlv("00 05", writer.clear()) - - -def tlv303(token: bytes) -> bytes: - """ - TLV_0303_UnknownTag - - :param token: PcToken 0060 From 0819 Protocol Package - - """ - writer = binary.Writer() - - writer.write(token) - - return _tlv("03 03", writer.clear()) - - -def tlv006( - uin: int, - tgt_key: bytes, - md5_once: bytes, - md5_twice: bytes, - login_time: bytes, - local_ip: bytes, - computer_id: bytes, - tgtgt: bytes - -) -> bytes: - """ - TLV_0006_TGTGT - - :param uin: QQ Number - - :param tgt_key: TGTKey - - :param md5_once: The password MD5 is encrypted once - - :param md5_twice: The password MD5 is encrypted twice - - :param login_time: Time to Login QQ Server - - :param local_ip: Local IP Address - - :param computer_id: Computer device ID - - :param tgtgt: TGTGT - - """ - writer = binary.Writer() - - if tgtgt: - writer.write(tgtgt) - else: - writer.write(utils.randbytes(4)) - writer.write_hex("00 02") - writer.write_int32(uin) - writer.write_hex(const.SSO_VERSION) # SSO Version - writer.write_hex(const.SERVICEID) # ServiceId - writer.write_hex(const.CLIENT_VERSION) # Client Version - writer.write_hex("00 00") - writer.write_hex("00") # Don't remember password - writer.write(md5_once) - writer.write(login_time) - writer.write_hex("00 00 00 00 00 00 00 00 00 00 00 00 00") - writer.write(local_ip) - writer.write_hex("00 00 00 00 00 00 00 00 00 10") - writer.write(computer_id) - writer.write(tgt_key) - - tea = binary.QQTea(md5_twice) - return _tlv("00 06", tea.encrypt(writer.clear())) - - -def tlv015() -> bytes: - """ - TLV_0015_ComputerGuid - - """ - writer = binary.Writer() - - writer.write_hex("00 01") # wSubVer - - writer.write_byte(1) - writer.write_hex("74 83 F2 C3") # CRC-32 ComputerGuid - writer.write_int16(16) # ComputerGuid Length - - # ComputerGuid Data - writer.write_hex("14 FE 77 FC 00 00 00 00 00 00 00 00 00 00 00 00") - - writer.write_byte(2) - writer.write_hex("17 65 6E 9D") # CRC-32 ComputerGuidEX - writer.write_int16(16) # ComputerGuidEX Length - - # ComputerGuidEX Data - writer.write_hex("78 8A 33 DD 00 76 A1 78 EB 8E 5B BB FF 17 D0 10") - - return _tlv("00 15", writer.clear()) - - -def tlv01a(tgtkey: bytes) -> bytes: - """ - TLV_001A_GTKeyTGTGTCryptedData - - :param tgtkey: Tgtkey - - """ - - tea = binary.QQTea(tgtkey) - return _tlv("00 1A", tea.encrypt(tlv015())) - - -def tlv018(uin: int, redirect_times: int) -> bytes: - """ - TLV_0018_Ping - - :param uin: QQ Number - - :param redirect_times: Times of QQ Server redirects - - """ - writer = binary.Writer() - - writer.write_hex("00 01") # wSubVer - writer.write_hex(const.SSO_VERSION) # SSO Version - writer.write_hex(const.SERVICEID) # ServiceId - writer.write_hex(const.CLIENT_VERSION) # Client Version - writer.write_int32(uin) - writer.write_int16(redirect_times) - writer.write_hex("00 00") - - return _tlv("00 18", writer.clear()) - - -def tlv019() -> bytes: - """ - TLV_0019_Ping - - """ - writer = binary.Writer() - - writer.write_hex("00 01") # wSubVer - writer.write_hex(const.SSO_VERSION) # SSO Version - writer.write_hex(const.SERVICEID) # ServiceId - writer.write_hex(const.CLIENT_VERSION) # Client Version - writer.write_hex("00 00") - - return _tlv("00 19", writer.clear()) - - -def tlv305() -> bytes: - """ - Tlv_0305_QRCodeParams - - """ - writer = binary.Writer() - - writer.write_hex("00 00 00 00") - writer.write_hex("00 00 00 05") - writer.write_hex("00 00 00 04") - writer.write_hex("00 00 00 00") - writer.write_hex("00 00 00 48") - writer.write_hex("00 00 00 02") - writer.write_hex("00 00 00 02") - writer.write_hex("00 00") - - return _tlv("03 05", writer.clear()) - - -def tlv114() -> bytes: - """ - TLV_0114_DHParams - - """ - writer = binary.Writer() - - writer.write_hex(const.EDCH_VERSION) # Edch Version - writer.write_int16(25) # PublicKey Length - writer.write_hex(const.PUBLICKEY) # PublicKey Data - - return _tlv("01 14", writer.clear()) - - -def tlv103() -> bytes: - """ - TLV_0103_SID - - """ - writer = binary.Writer() - - writer.write_hex("00 01 00 10") - writer.write(utils.randbytes(16)) - - return _tlv("01 03", writer.clear()) - - -def tlv312() -> bytes: - """ - TLV_0312_Misc_Flag - - """ - writer = binary.Writer() - - writer.write_hex("01 00 00 00 00") - - return _tlv("03 12", writer.clear()) - - -def tlv508() -> bytes: - """ - TLV_0508_UnknownTag - - """ - writer = binary.Writer() - - writer.write_hex("01 00 00 00 02") - - return _tlv("05 08", writer.clear()) - - -def tlv313() -> bytes: - """ - TLV_0313_GUID_Ex - - """ - writer = binary.Writer() - - writer.write_hex("01") - writer.write_byte(1) # GUID_Ex Count - - writer.write_hex("02") # GUID_Ex Index - writer.write_int16(16) # GUID_Ex Length - # GUID_Ex Data - writer.write_hex("EE 47 7F A4 BC D6 EE 65 02 65 4D E9 43 38 4C 3D") - - writer.write_hex("00 00 00 EB") # System Tick - - return _tlv("03 13", writer.clear()) - - -def tlv102(token: bytes) -> bytes: - """ - TLV_0102_Official - - : param token: Token 0038 From 0825 Protocol Package - - """ - writer = binary.Writer() - - writer.write_hex("00 01") - writer.write(utils.randbytes(16)) - writer.write_int16(len(token)) - writer.write(token) - writer.write_hex("00 14") - writer.write(utils.randbytes(20)) - - return _tlv("01 02", writer.clear()) - - -def tlv309( - server_ip: bytes, - redirection_history: bytes, - redirection_times: int -) -> bytes: - """ - TLV_0309_Ping_Strategy - - :param server_ip: QQ Server IP Address - - :param redirect_history: History of QQ Server redirects - - :param redirect_times: Times of QQ Server redirects - - """ - writer = binary.Writer() - - writer.write_hex("00 01") - writer.write(server_ip) - - if redirection_times: - writer.write_byte(redirection_times) - writer.write(redirection_history) - else: - writer.write_hex("00 02") - - return _tlv("03 09", writer.clear()) - - -def tlv036() -> bytes: - """ - TLV_0036_LoginReason - - """ - writer = binary.Writer() - - writer.write_hex("00 02") - writer.write_hex("00 01") - writer.write_hex("00 00 00 00 00 00 00 00 00 00 00 00 00 00") - - return _tlv("00 36", writer.clear()) - - -def tlv007(token: bytes) -> bytes: - """ - TLV_0007_TGT - - :param token: PcToken 0088 From 0836 Protocol Package - - """ - writer = binary.Writer() - - writer.write(token) - - return _tlv("00 07", writer.clear()) - - -def tlv00c(server_ip: bytes) -> bytes: - """ - TLV_000C_PingRedirect - - :param server_ip: QQ Server IP Address - - """ - writer = binary.Writer() - - writer.write_int16(2) - writer.write_hex("00 01 00 00 00 00 00 00 00 00") - writer.write(server_ip) - writer.write_int16(80) - writer.write_hex("00 00 00 00") - - return _tlv("00 0C", writer.clear()) - - -def tlv01f() -> bytes: - """ - TLV_001F_DeviceID - - """ - writer = binary.Writer() - - writer.write_hex("00 01") - writer.write(utils.randbytes(32)) - - return _tlv("00 1F", writer.clear()) - - -def tlv105() -> bytes: - """ - TLV_0105_m_vec0x12c - - """ - writer = binary.Writer() - - writer.write_int16(1) - writer.write_hex("01 02") - - writer.write_int16(20) - writer.write_hex("01 01") - writer.write_int16(16) - writer.write(utils.randbytes(16)) - - writer.write_int16(20) - writer.write_hex("01 02") - writer.write_int16(16) - writer.write(utils.randbytes(16)) - - return _tlv("01 05", writer.clear()) - - -def tlv10b() -> bytes: - """ - TLV_010B_QDLoginFlag - - """ - writer = binary.Writer() - - writer.write_hex("00 02") - writer.write(utils.randbytes(17)) # (Random)ClientMd5 + QDFlag - writer.write_hex("10 00 00 00 00 00 00 00 02") - - writer.write_hex("00 63") - writer.write_hex("3E 00 63 02") - writer.write_hex(const.DWQD_VERSION) - writer.write_hex("00 04 00") - writer.write(utils.randbytes(2)) - writer.write_hex("00 00 00 00") - writer.write(utils.randbytes(16)) - - writer.write_hex("01 00") - writer.write_hex("00 00 00 00") - writer.write_hex("00 00") - writer.write_hex("00 00 00 00 00 00 00 00") - - writer.write_hex("00 00 00 01") - writer.write_hex("00 00 00 01") - writer.write_hex("00 00 00 01") - writer.write_hex("00 00 00 01") - writer.write_hex("00 FE 26 81 75 EC 2A 34 EF") - - # QDDATA - writer.write_hex("02 3E 50 39 6D B1 AF CC 9F EA 54 E1") - writer.write_hex("70 CC 6C 9E 4E 63 8B 51 EC 7C 84 5C") - - writer.write_hex("68") - writer.write_hex("00 00 00 00") - - return _tlv("01 0B", writer.clear()) - - -def tlv02d(local_ip: bytes) -> bytes: - """ - TLV_002D_LocalIP - - """ - writer = binary.Writer() - - writer.write_hex("00 01") - writer.write(local_ip) - - return _tlv("00 2D", writer.clear()) diff --git a/pcqq/binary/reader.py b/pcqq/binary/reader.py deleted file mode 100644 index 5b97fa6..0000000 --- a/pcqq/binary/reader.py +++ /dev/null @@ -1,26 +0,0 @@ -import io -import re - - -class Reader: - def __init__(self, initial_bytes: bytes = b'') -> None: - self.raw = io.BytesIO(initial_bytes) - - def tell(self) -> int: - return len(self.raw.getvalue()) - self.raw.tell() - - def read(self, size: int = -1) -> bytes: - return self.raw.read(size) - - def read_hex(self, size: int = -1) -> str: - hex = self.read(size).hex() - return " ".join(re.findall('.{2}', hex.upper())) - - def read_int16(self) -> int: - return int.from_bytes(self.read(2), 'big') - - def read_int32(self) -> int: - return int.from_bytes(self.read(4), 'big') - - def read_byte(self) -> int: - return self.read(1)[0] diff --git a/pcqq/binary/writer.py b/pcqq/binary/writer.py deleted file mode 100644 index db7ff6e..0000000 --- a/pcqq/binary/writer.py +++ /dev/null @@ -1,52 +0,0 @@ -import io - - -def varint(num: int) -> bytes: - val = bytearray() - while num > 127: - val.append(0x80 | num & 0x7F) - num = num >> 7 - val.append(num) - return bytes(val) - - -class Writer: - def __init__(self) -> None: - self.raw = io.BytesIO() - - def clear(self) -> bytes: - data = self.raw.getvalue() - self.raw.close() - return data - - def write(self, bin: bytes) -> None: - self.raw.write(bin) - - def write_int16(self, num: int) -> None: - self.write(num.to_bytes(2, 'big')) - - def write_int32(self, num: int) -> None: - self.write(num.to_bytes(4, 'big')) - - def write_hex(self, hex: str) -> None: - self.write(bytes.fromhex(hex)) - - def write_byte(self, byte: int) -> None: - self.write(byte.to_bytes(1, 'big')) - - def write_varint(self, num: int) -> None: - self.write(varint(num)) - - def write_pbint(self, num: int, mark: int, isid: bool = True): - mark = 8 * mark if isid else mark - self.write_varint(mark) - self.write_varint(num) - - def write_pbdata(self, data: bytes, mark: int, isid: bool = True): - mark = 2 + 8 * mark if isid else mark - self.write_varint(mark) - self.write_byte(len(data)) - self.write(data) - - def write_pbhex(self, hex: str, mark: int, isid: bool = True): - return self.write_pbdata(bytes.fromhex(hex), mark, isid) diff --git a/pcqq/bot.py b/pcqq/bot.py deleted file mode 100644 index 8851a22..0000000 --- a/pcqq/bot.py +++ /dev/null @@ -1,123 +0,0 @@ -import os -import asyncio - -import pcqq.handle as hd -import pcqq.client as cli -import pcqq.const as const -import pcqq.network as net -import pcqq.session as ses -import pcqq.logger as logger -# cli = cli.QQClient() - - -async def exec_rules(rules, session): - # 判断是否执行插件 - for rule in rules: - if not await rule(session): - return False - return True - - -async def plugin_handle(body): - session = ses.Session() - typ = body[18:20].hex().upper() - if typ == const.EVENT_GROUP_MSG: - await hd.group_msg_handle(session, body) - elif typ == const.EVENT_FRIEND_MSG: - await hd.friend_msg_handle(session, body) - elif typ == const.EVENT_GROUP_INCREASE: - await hd.group_increase_handle(session, body) - elif typ == const.EVENT_GROUP_DECREASE: - await hd.group_decrease_handle(session, body) - elif typ == const.EVENT_OTHRE: - await hd.other_handle(session, body) - else: - return - - if session.user_id == session.self_id and session.event_type == "group_msg": - return # 忽略自身群消息 - - for plugin in cli.plugins: - - # 判断是否位临时插件 - if plugin.temp: - del cli.plugins[cli.plugins.index(plugin)] - - # 判断是否执行插件 - if await exec_rules(plugin.rules, session): - try: - await plugin.handle(session) - except Exception as err: - logger.error(f"插件 {plugin.handle.__name__} 运行时发生异常 -> {err}") - - # 判断是否阻断后续插件执行 - if plugin.block: - break - -async def event_handle(cmd:str, sequence:bytes, body:bytes): - await cli.write_packet( - cmd, - const.BODY_VERSION, - body[0:16], - sequence=sequence - ) # 初步进行回执 - - if cmd == "03 52": - """私聊图片上传响应""" - callback = cli.waiter.pop(sequence.hex()) - await callback(body) - elif cmd == "03 88": - """群聊图片上传响应""" - callback = cli.waiter.pop(sequence.hex()) - await callback(body) - else: - await plugin_handle(body) - -async def main_handle(): - while True: - try: - cmd, sequence, _, body = await cli.read_packet() - except: - continue - cli.run_future(event_handle(cmd, sequence, body)) - -async def keep_heatbeat(): - while True: - cli.run_future(net.heatbeat()) - await asyncio.sleep(40.0) - - -def only_login(uin: int = 0, password: str = ""): - """ - 仅登录, 不处理事件 - - :param uin: 用于登录的QQ号(留空则使用扫码登录) - - :param password: 登录QQ号的密码(留空则使用扫码登录) - - """ - cli.run(cli.client.init()) - - if os.path.exists("session.token"): - cli.run(net.token_login()) - elif uin and password: - cli.run(net.password_login(uin, password)) - else: - cli.run(net.qrcode_login()) - - -def run_bot(uin: int = 0, password: str = ""): - """ - 登录机器人,并开始处理事件 - - :param uin: 用于登录的QQ号(留空则使用扫码登录) - - :param password: 登录QQ号的密码(留空则使用扫码登录) - - """ - only_login(uin, password) - cli.run_future(keep_heatbeat()) - - cli.run_future(main_handle()) - logger.info(f"已导入 {len(cli.plugins)} 组插件,开始处理事件.....") - cli.client.loop.run_forever() diff --git a/pcqq/client.py b/pcqq/client.py deleted file mode 100644 index 92ab049..0000000 --- a/pcqq/client.py +++ /dev/null @@ -1,319 +0,0 @@ -import sys -import json -import base64 -import random -import socket -import asyncio -from urllib import parse - -import pcqq.utils as utils -import pcqq.const as const -import pcqq.logger as logger -import pcqq.binary as binary - - -class AsyncTCPClient: - def __init__(self) -> None: - domain = random.choice([ - "tcpconn.tencent.com", - "tcpconn2.tencent.com", - "tcpconn3.tencent.com", - "tcpconn4.tencent.com" - ]) - self.host: str = socket.gethostbyname(domain) - self.port: int = const.TCP_PORT - self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() - - async def init(self) -> None: - self.writer: asyncio.StreamWriter = None - self.reader: asyncio.StreamReader = None - self.reader, self.writer = await asyncio.open_connection( - host=self.host, - port=self.port, - loop=self.loop - ) - logger.info(f"正在创建 TCP 连接: {self.host}:{self.port} -> 成功") - - async def send(self, data: bytes) -> None: - size = len(data) + 2 - self.writer.write(size.to_bytes(2, "big") + data) - - async def recv(self) -> bytes: - data = await self.reader.read(const.BUF_SIZE) - return data[2:] - - -class QQStruct: - local_ip: bytes = b'' - - server_ip: bytes = b'' - - uin: int = 0 - - password: bytes = b'' - - nickname: str = '' - - is_scancode: bool = False - - login_time: bytes = b'' - - redirection_times: int = 0 - - redirection_history: bytes = b'' - - tgt_key: bytes = b'' - - tgtgt_key: bytes = b'' - - session_key: bytes = b'' - - bkn: int = 0 - - skey: str = '' - - cookie: str = '' - - pckey_for_0819: bytes = b'' - - pckey_for_0828_send: bytes = b'' - - pckey_for_0828_recv: bytes = b'' - - token_by_scancode: bytes = b'' - - token_0038_from_0825: bytes = b'' - - token_0038_from_0818: bytes = b'' - - token_0038_from_0836: bytes = b'' - - token_0088_from_0836: bytes = b'' - - def save_token(self, path: str): - file = open(path, mode='wb') - - file.write(b'DawnNights'.join([ - self.server_ip, - self.uin.to_bytes(4, 'big'), - self.nickname.encode(), - self.token_0038_from_0836, - self.token_0088_from_0836, - self.pckey_for_0828_send, - self.pckey_for_0828_recv - ])) - - logger.info(f'已保存登录Token至文件 {path}') - file.close() - - def load_token(self, path: str): - file = open(path, mode='rb') - tokens = file.read().split(b'DawnNights') - - self.pckey_for_0828_recv = tokens.pop() - self.pckey_for_0828_send = tokens.pop() - self.token_0088_from_0836 = tokens.pop() - self.token_0038_from_0836 = tokens.pop() - self.nickname = tokens.pop().decode() - self.uin = int.from_bytes(tokens.pop(), 'big') - self.server_ip = tokens.pop() - - logger.info(f'尝试通过 {path} 进行重连...') - file.close() - - -class HttpResponse: - ok: bool = False - url: str = "" - headers: dict = {} - content: bytes = b"" - status_code: int = 0 - links: parse.ParseResult = None - - def __repr__(self): - return "<%s Response %d>" % ( - self.links.scheme.upper(), - self.status_code - ) - - def json(self) -> dict: - return json.loads(self.content) - - def base64(self) -> str: - return base64.b64encode(self.content).decode() - - -class QQClient(QQStruct): - def __init__(self) -> None: - self.waiter: dict = {} - self.plugins: list = [] - self.client: AsyncTCPClient = AsyncTCPClient() - self.teaer: binary.QQTea = binary.QQTea(bytes()) - - self.tgt_key = utils.randbytes(16) - self.server_ip = socket.inet_aton(self.client.host) - - def run_until_complete(coro): - task = self.client.loop.create_task(coro) - self.client.loop.run_until_complete(task) - return task.result() - self.run = run_until_complete - - def run_coroutine_threadsafe(coro): - return asyncio.run_coroutine_threadsafe( - coro=coro, - loop=self.client.loop, - ) - self.run_future = run_coroutine_threadsafe - - self.cache: utils.SqliteDB = utils.SqliteDB("cache.db") - if self.cache.count("ginfo") == -1: - self.cache.create( - "ginfo", - "name text", "owner integer", - "group_id integer", "board text", - "intro text", "mem_num integer" - ) - if self.cache.count("gmember") == -1: - self.cache.create( - "gmember", - "cord text", - "user_id integer", - "name text", - "group_id integer", - "op boolen" - ) - - async def write_packet(self, cmd: str, version: str, body: bytes, sequence: bytes = utils.randbytes(2)) -> None: - writer = binary.Writer() - writer.write_hex(const.HEADER) - - writer.write_hex(cmd) - writer.write(sequence) - writer.write_int32(self.uin) - writer.write_hex(version) - - if self.session_key: - writer.write(self.teaer.encrypt(body)) - else: - writer.write(body) - - writer.write_hex(const.TAIL) - await self.client.send(writer.clear()) - - async def read_packet(self) -> tuple: - reader = binary.Reader(await self.client.recv()) - reader.read(3) # packet header - - cmd = reader.read_hex(2) - sequence = reader.read(2) - uin = reader.read_int32() # qq uin - reader.read(3) # unknow - - if self.session_key: - body = self.teaer.decrypt(reader.read()[:-1]) - else: - body = reader.read()[:-1] - - return cmd, sequence, uin, body - - async def webget(self, url: str, headers: dict = {}, **params) -> HttpResponse: - ret = parse.urlparse(url) - if ret.scheme == "http": - reader, writer = await asyncio.open_connection( - host=ret.netloc, - port=80, - ) - elif ret.scheme == "https": - reader, writer = await asyncio.open_connection( - host=ret.netloc, - port=443, - ssl=True, - ) - - params = parse.urlencode(params) - url = url + "?" + params if params else url - request = f"GET {url} HTTP/1.1\r\n" - - headers["Host"] = ret.netloc - headers["Connection"] = "close" - for key, value in headers.items(): - request += f"{key}: {value}\r\n" - writer.write((request + "\r\n").encode()) - - response = HttpResponse() - response.url = url - response.links = ret - - # Response Status - line = await reader.readline() - response.status_code = int(line.split(b" ")[1]) - response.ok = response.status_code == 200 - - # Response Headers - line = (await reader.readline()).replace(b"\r\n", b"").decode() - while line: - key, value = line.split(": ") - response.headers[key] = value - line = (await reader.readline()).replace(b"\r\n", b"").decode() - - # Response Data - response.content = await reader.read() - - writer.close() - return response - - async def webpost(self, url: str, headers: dict = {}, **params) -> HttpResponse: - ret = parse.urlparse(url) - if ret.scheme == "http": - reader, writer = await asyncio.open_connection( - host=ret.netloc, - port=80, - ) - elif ret.scheme == "https": - reader, writer = await asyncio.open_connection( - host=ret.netloc, - port=443, - ssl=True, - ) - else: - raise Exception("调用方法 webpost 时传入的 url 参数有误") - - request = f"POST {url} HTTP/1.1\r\n" - if "data" in params and isinstance(params["data"], bytes): - post_data = params["data"] - else: - post_data = parse.urlencode(params).encode() - - headers["Host"] = ret.netloc - headers["Connection"] = "close" - headers["Content-Length"] = len(post_data) - for key, value in headers.items(): - request += f"{key}: {value}\r\n" - - writer.write((request + "\r\n").encode() + post_data) - - response = HttpResponse() - response.url = url - response.links = ret - - # Response Status - line = await reader.readline() - response.status_code = int(line.split(b" ")[1]) - response.ok = response.status_code == 200 - - # Response Headers - line = (await reader.readline()).replace(b"\r\n", b"").decode() - while line: - key, value = line.split(": ") - response.headers[key] = value - line = (await reader.readline()).replace(b"\r\n", b"").decode() - - # Response Data - response.content = await reader.read() - - writer.close() - return response - - -sys.modules[__name__] = QQClient() diff --git a/pcqq/const/__init__.py b/pcqq/const/__init__.py deleted file mode 100644 index ad862a0..0000000 --- a/pcqq/const/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .ctx import * -from .packet import * -from .typing import * \ No newline at end of file diff --git a/pcqq/const/ctx.py b/pcqq/const/ctx.py deleted file mode 100644 index b7f3dde..0000000 --- a/pcqq/const/ctx.py +++ /dev/null @@ -1,280 +0,0 @@ -QRCODE_SIZE = 37 -BUF_SIZE = 1024 * 15 - -TCP_PORT = 443 -UDP_PORT = 8000 -HTTP_PORT = 80 -HTTPS_PORT = 443 - -MUSIC_CODE = """""" - -FACE_AMAZED = 0 # 表情-惊讶 -FACE_CURLEDLIP = 1 # 表情-撇嘴 -FACE_LUST = 2 # 表情-色 -FACE_DULL = 3 # 表情-发呆 -FACE_PROUD = 4 # 表情-得意 -FACE_WEEP = 5 # 表情-流泪 -FACE_SHY = 6 # 表情-害羞 -FACE_SHUTUP = 7 # 表情-闭嘴 -FACE_SLEEP = 8 # 表情-睡 -FACE_CRY = 9 # 表情-大哭 -FACE_AWKWARD = 10 # 表情-尴尬 -FACE_ANGRY = 11 # 表情-发怒 -FACE_NAUGHTY = 12 # 表情-调皮 -FACE_TEETH = 13 # 表情-呲牙 -FACE_SMILE = 14 # 表情-微笑 -FACE_UNWELL = 15 # 表情-难过 -FACE_COOL = 16 # 表情-酷 -# { -# 14: "微笑", -# 1: "撇嘴", -# 2: "色", -# 3: "发呆", -# 4: "得意", -# 5: "流泪", -# 6: "害羞", -# 7: "闭嘴", -# 8: "睡", -# 9: "大哭", -# 10: "尴尬", -# 11: "发怒", -# 12: "调皮", -# 13: "呲牙", -# 0: "惊讶", -# 15: "难过", -# 16: "酷", -# 96: "冷汗", -# 18: "抓狂", -# 19: "吐", -# 20: "偷笑", -# 21: "可爱", -# 22: "白眼", -# 23: "傲慢", -# 24: "饥饿", -# 25: "困", -# 26: "惊恐", -# 27: "流汗", -# 28: "憨笑", -# 29: "悠闲", -# 30: "奋斗", -# 31: "咒骂", -# 32: "疑问", -# 33: "嘘", -# 34: "晕", -# 35: "折磨", -# 36: "衰", -# 37: "骷髅", -# 38: "敲打", -# 39: "再见", -# 97: "擦汗", -# 98: "抠鼻", -# 99: "鼓掌", -# 100: "糗大了", -# 101: "坏笑", -# 102: "左哼哼", -# 103: "右哼哼", -# 104: "哈欠", -# 105: "鄙视", -# 106: "委屈", -# 107: "快哭了", -# 108: "阴险", -# 305: "右亲亲", -# 109: "左亲亲", -# 110: "吓", -# 111: "可怜", -# 172: "眨眼睛", -# 182: "笑哭", -# 179: "doge", -# 173: "泪奔", -# 174: "无奈", -# 212: "托腮", -# 175: "卖萌", -# 178: "斜眼笑", -# 177: "喷血", -# 180: "惊喜", -# 181: "骚扰", -# 176: "小纠结", -# 183: "我最美", -# 245: "加油必胜", -# 246: "加油抱抱", -# 247: "口罩护体", -# 260: "搬砖中", -# 261: "忙到飞起", -# 262: "脑阔疼", -# 263: "沧桑", -# 264: "捂脸", -# 265: "辣眼睛", -# 266: "哦哟", -# 267: "头秃", -# 268: "问号脸", -# 269: "暗中观察", -# 270: "emm", -# 271: "吃瓜", -# 272: "呵呵哒", -# 277: "汪汪", -# 307: "喵喵", -# 306: "牛气冲天", -# 281: "无眼笑", -# 282: "敬礼", -# 283: "狂笑", -# 284: "面无表情", -# 285: "摸鱼", -# 293: "摸锦鲤", -# 286: "魔鬼笑", -# 287: "哦", -# 288: "请", -# 289: "睁眼", -# 294: "期待", -# 295: "拿到红包", -# 296: "真好", -# 297: "拜谢", -# 298: "元宝", -# 299: "牛啊", -# 300: "胖三斤", -# 301: "好闪", -# 303: "右拜年", -# 302: "左拜年", -# 304: "红包包", -# 322: "拒绝", -# 323: "嫌弃", -# 311: "打call", -# 312: "变形", -# 313: "嗑到了", -# 314: "仔细分析", -# 315: "加油", -# 316: "我没事", -# 317: "菜狗", -# 318: "崇拜", -# 319: "比心", -# 320: "庆祝", -# 321: "老色痞", -# 49: "拥抱", -# 66: "爱心", -# 63: "玫瑰", -# 64: "凋谢", -# 187: "幽灵", -# 146: "爆筋", -# 116: "示爱", -# 67: "心碎", -# 60: "咖啡", -# 185: "羊驼", -# 192: "红包", -# 137: "鞭炮", -# 138: "灯笼", -# 136: "双喜", -# 76: "赞", -# 124: "OK", -# 118: "抱拳", -# 78: "握手", -# 119: "勾引", -# 79: "胜利", -# 120: "拳头", -# 121: "差劲", -# 77: "踩", -# 122: "爱你", -# 123: "NO", -# 201: "点赞", -# 203: "托脸", -# 204: "吃", -# 202: "无聊", -# 200: "拜托", -# 194: "不开心", -# 193: "大笑", -# 197: "冷漠", -# 211: "我不看", -# 210: "飙泪", -# 198: "呃", -# 199: "好棒", -# 207: "花痴", -# 205: "送花", -# 206: "害怕", -# 208: "小样儿", -# 308: "求红包", -# 309: "谢红包", -# 310: "新年烟花", -# 290: "敲开心", -# 291: "震惊", -# 292: "让我康康", -# 226: "拍桌", -# 215: "糊脸", -# 237: "偷看", -# 214: "啵啵", -# 235: "颤抖", -# 222: "抱抱", -# 217: "扯一扯", -# 221: "顶呱呱", -# 225: "撩一撩", -# 241: "生日快乐", -# 227: "拍手", -# 238: "扇脸", -# 240: "喷脸", -# 229: "干杯", -# 216: "拍头", -# 218: "舔一舔", -# 233: "掐一掐", -# 219: "蹭一蹭", -# 244: "扔狗", -# 232: "佛系", -# 243: "甩头", -# 223: "暴击", -# 279: "打脸", -# 280: "击掌", -# 231: "哼", -# 224: "开枪", -# 278: "汗", -# 236: "啃头", -# 228: "恭喜", -# 220: "拽炸天", -# 239: "原谅", -# 242: "头撞击", -# 230: "嘲讽", -# 234: "惊呆", -# 273: "我酸了", -# 75: "月亮", -# 74: "太阳", -# 46: "猪头", -# 112: "菜刀", -# 56: "刀", -# 169: "手枪", -# 171: "茶", -# 59: "便便", -# 144: "喝彩", -# 147: "棒棒糖", -# 89: "西瓜", -# 61: "饭", -# 148: "喝奶", -# 274: "太南了", -# 113: "啤酒", -# 140: "K歌", -# 53: "蛋糕", -# 188: "蛋", -# 55: "炸弹", -# 184: "河蟹", -# 158: "钞票", -# 54: "闪电", -# 69: "礼物", -# 190: "菊花", -# 151: "飞机", -# 145: "祈祷", -# 117: "瓢虫", -# 168: "药", -# 114: "篮球", -# 115: "乒乓", -# 57: "足球", -# 41: "发抖", -# 125: "转圈", -# 42: "爱情", -# 43: "跳跳", -# 86: "怄火", -# 129: "挥手", -# 85: "飞吻", -# 126: "磕头", -# 128: "跳绳", -# 130: "激动", -# 127: "回头", -# 132: "献吻", -# 134: "右太极", -# 133: "左太极", -# 131: "街舞", -# 276: "辣椒酱" -# } diff --git a/pcqq/const/packet.py b/pcqq/const/packet.py deleted file mode 100644 index 0e8ea9d..0000000 --- a/pcqq/const/packet.py +++ /dev/null @@ -1,25 +0,0 @@ -HEADER = "02 36 39" - -TAIL = "03" - -SERVICEID = "00 00 00 01" - -RANDKEY = "B6 9F 79 EC E5 77 47 A6 99 FA A8 3C 56 E5 E8 3E" - -SHAREKEY = "FD 0B 79 78 31 E6 88 54 FC FA EA 84 52 9C 7D 0B" - -PUBLICKEY = "03 94 3D CB E9 12 38 61 EC F7 AD BD E3 36 91 91 07 01 50 BE 50 39 1C D3 32" - -EDCH_VERSION = "01 02" - -SSO_VERSION = "00 00 04 4C" - -CLIENT_VERSION = "00 00 15 51" - -DWQD_VERSION = "04 00 03 07" - -BODY_VERSION = "02 00 00 00 01 01 01 00 00 67 B7" - -STRUCT_VERSION = "03 00 00 00 01 01 01 00 00 67 B7 00 00 00 00" - -FUNC_VERSION = "04 00 00 00 01 01 01 00 00 6A 0F 00 00 00 00 00 00 00 00" \ No newline at end of file diff --git a/pcqq/const/typing.py b/pcqq/const/typing.py deleted file mode 100644 index 349cc1a..0000000 --- a/pcqq/const/typing.py +++ /dev/null @@ -1,20 +0,0 @@ -MSG_TEXT = 0x01 # 纯文本 -MSG_FACE = 0x02 # 表情 -MSG_IMAGE_GROUP = 0X03 # 群聊图片 -MSG_IMAGE_FRIEND = 0X06 # 私聊图片 -MSG_XML = 0x14 # XML卡片 -MSG_JSON = 0x25 # JSON卡片 - -STATE_ONLINE = 10 # 上线 -STATE_LEAVE = 30 # 离开 -STATE_INVISIBLE = 40 # 隐身 -STATE_BUSY = 50 # 忙碌 -STATE_CALLME = 60 # Q我吧 -STATE_UNDISTURB = 70 # 请勿打扰 - -EVENT_GROUP_MSG = "0052" # 群消息 -EVENT_FRIEND_MSG = "00A6" # 好友消息 -EVENT_TEMP_MSG = "008D" # 临时会话消息 -EVENT_GROUP_INCREASE = "0021" # 群人数增加 -EVENT_GROUP_DECREASE = "0022" # 群人数减少 -EVENT_OTHRE = "02DC" # 禁言、匿名等事件 diff --git a/pcqq/handle.py b/pcqq/handle.py deleted file mode 100644 index 0220f9c..0000000 --- a/pcqq/handle.py +++ /dev/null @@ -1,272 +0,0 @@ -import pcqq.network as net -import pcqq.utils as utils -import pcqq.const as const -import pcqq.logger as logger -import pcqq.binary as binary - - -async def msg_handle(session, reader: binary.Reader): - reader.read(16) - reader.read(reader.read_int16()) - reader.read_int16() - - session.raw_message.clear() - while reader.tell() > 0: - typ = reader.read_byte() - msgread = binary.Reader(reader.read(reader.read_int16())) - - if typ == const.MSG_TEXT: - msgread.read_byte() - data = msgread.read(msgread.read_int16()) - - if msgread.tell(): - msgread.read(10) - uid = msgread.read_int32() - session.message += f"[PQ:at,qq={uid}]" - session.raw_message.append({ - "type": "at", - "qq": uid - }) - else: - text = data.decode() - session.message += text - session.raw_message.append(text) - - elif typ == const.MSG_FACE: - msgread.read(3) - face_id = msgread.read_byte() - session.message += f"[PQ:face,id={face_id}]" - session.raw_message.append({ - "type": "face", - "id": face_id - }) - - elif typ in (const.MSG_IMAGE_GROUP, const.MSG_IMAGE_FRIEND): - msgread.read_byte() - id = msgread.read(msgread.read_int16()).decode().upper() - id = id[:-4].replace("-", "").replace("{", "").replace("}", "") - session.message += f"[PQ:image,file={id}]" - session.raw_message.append({ - "type": "image", - "url": f"https://gchat.qpic.cn/gchatpic_new/0/0-0-{id}/0?term=3" - }) - - -async def group_msg_handle(session, body: bytes): - reader = binary.Reader(body) - session.event_type = "group_msg" - - reader.read(4) - session.self_id = reader.read_int32() - - reader.read(12) - reader.read(reader.read_int32()) - session.group_id = reader.read_int32() - - reader.read_byte() - session.user_id = reader.read_int32() - session.msg_id = reader.read_int32() - session.timestamp = reader.read_int32() - - reader.read(8) - reader.read(16) - await msg_handle(session, reader) - - if session.user_id != session.self_id: - logger.info(f"收到群聊 %s(%d) 消息 %s(%d): %s" % ( - await net.get_group_cache(session.group_id), - session.group_id, - await net.get_user_cache(session.user_id, session.group_id), - session.user_id, - session.message - )) - await net.group_receipt(session.group_id, session.msg_id) - - -async def friend_msg_handle(session, body: bytes): - reader = binary.Reader(body) - session.event_type = "friend_msg" - - session.user_id = reader.read_int32() - session.self_id = reader.read_int32() - - reader.read(12) - reader.read(reader.read_int32()) - reader.read(26 + 2) - - session.msg_id = reader.read_int16() - session.timestamp = reader.read_int32() - - reader.read(6 + 4 + 9) - await msg_handle(session, reader) - - logger.info(f"收到好友消息 %s(%d): %s" % ( - await net.get_user_cache(session.user_id), - session.user_id, session.message - )) - await net.friend_receipt(session.user_id, session.timestamp) - - -async def group_increase_handle(session, body: bytes): - reader = binary.Reader(body) - session.event_type = "group_increase" - - session.group_id = utils.group_from_gid(reader.read_int32()) - session.self_id = reader.read_int32() - - reader.read(12) - reader.read(reader.read_int32() + 5) - session.target_id = reader.read_int32() - sign = reader.read_byte() - session.user_id = reader.read_int32() - - if sign == 0x03: - logger.info("收到群 %s(%d) 事件: %s(%d) 邀请 %s(%d) 加入群聊" % ( - await net.get_group_cache(session.group_id), session.group_id, - await net.get_user_cache( - session.user_id, - session.group_id - ), session.user_id, - await net.get_user_cache(session.target_id), session.target_id - )) - - else: - logger.info("收到群 %s(%d) 事件: %s(%d) 同意 %s(%d) 加入群聊" % ( - await net.get_group_cache(session.group_id), session.group_id, - await net.get_user_cache( - session.user_id, - session.group_id - ), session.user_id, - - await net.get_user_cache(session.target_id), session.target_id - )) - - -async def group_decrease_handle(session, body: bytes): - reader = binary.Reader(body) - session.event_type = "group_decrease" - - session.group_id = utils.group_from_gid(reader.read_int32()) - session.self_id = reader.read_int32() - - reader.read(12) - reader.read(reader.read_int32() + 5) - session.target_id = reader.read_int32() - sign = reader.read_byte() - session.user_id = reader.read_int32() - - if sign == 0x01: - logger.info("收到群 %s(%d) 事件: 群主 %s(%d) 解散了该群" % ( - await net.get_group_cache(session.group_id), session.group_id, - - await net.get_user_cache( - session.user_id, - session.group_id - ), session.user_id, - )) - elif sign == 0x02: - logger.info("收到群 %s(%d) 事件: %s(%d) 退出了群聊" % ( - await net.get_group_cache(session.group_id), session.group_id, - - await net.get_user_cache( - session.target_id, - session.group_id - ), session.target_id - )) - elif sign == 0x03: - logger.info("收到群 %s(%d) 事件: %s(%d) 将 %s(%d) 踢出了群聊" % ( - await net.get_group_cache(session.group_id), session.group_id, - - await net.get_user_cache( - session.user_id, - session.group_id - ), session.user_id, - - await net.get_user_cache( - session.target_id, - session.group_id - ), session.target_id - )) - - -async def shutup_handle(session, reader: binary.Reader): - session.event_type = "group_shutup" - - reader.read(1) - session.user_id = reader.read_int32() - session.timestamp = reader.read_int32() - - reader.read(2) - session.target_id = reader.read_int32() - time = reader.read_int32() - - if session.target_id: - if time: - logger.info("收到群 %s(%d) 事件: %s(%d) 将 %s(%d) 禁言,预计 %s 禁言结束" % ( - await net.get_group_cache(session.group_id), session.group_id, - - await net.get_user_cache( - session.user_id, - session.group_id - ), session.user_id, - - await net.get_user_cache( - session.target_id, - session.group_id - ), session.target_id, - - utils.time_lapse(time) - )) - else: - logger.info("收到群 %s(%d) 事件: %s(%d) 将 %s(%d) 解除禁言" % ( - await net.get_group_cache(session.group_id), session.group_id, - - await net.get_user_cache( - session.user_id, - session.group_id - ), session.user_id, - - await net.get_user_cache( - session.target_id, - session.group_id - ), session.target_id, - )) - else: - if time: - logger.info("收到群 %s(%d) 事件: %s(%d) 开启了全体禁言" % ( - await net.get_group_cache(session.group_id), session.group_id, - - await net.get_user_cache( - session.user_id, - session.group_id - ), session.user_id, - )) - else: - logger.info("收到群 %s(%d) 事件: %s(%d) 解除了全体禁言" % ( - await net.get_group_cache(session.group_id), session.group_id, - - await net.get_user_cache( - session.user_id, - session.group_id - ), session.user_id, - )) - - -async def anonymous_handle(session, reader: binary.Reader): - session.event_type = "group_anonymous" - pass - - -async def other_handle(session, body: bytes): - reader = binary.Reader(body) - session.group_id = utils.group_from_gid(reader.read_int32()) - session.self_id = reader.read_int32() - - reader.read(12) - reader.read(reader.read_int32() + 4) - sign = reader.read_byte() - - if sign == 0x0c: - await shutup_handle(session, reader) - elif sign == 0x0e: - await anonymous_handle(session, reader) diff --git a/pcqq/logger.py b/pcqq/logger.py deleted file mode 100644 index 2b0deab..0000000 --- a/pcqq/logger.py +++ /dev/null @@ -1,16 +0,0 @@ -import sys -import logging - - -logging.basicConfig( - stream=sys.stdout, - level=logging.INFO, - datefmt="%Y/%m/%d-%H:%M:%S", - format='[%(name)s %(asctime)s] %(levelname)s: %(message)s' -) - -logger = logging.getLogger('PCQQ') -logger.fatal = sys.exit -logging.getLogger("asyncio").setLevel(logging.CRITICAL) - -sys.modules[__name__] = logger diff --git a/pcqq/message.py b/pcqq/message.py deleted file mode 100644 index d9d0cfa..0000000 --- a/pcqq/message.py +++ /dev/null @@ -1,231 +0,0 @@ -import zlib -import uuid -import json -import asyncio -from urllib import parse - -import pcqq.client as cli -import pcqq.network as net -import pcqq.utils as utils -import pcqq.const as const -import pcqq.binary as binary - - -def text(text: str) -> bytes: - writer = binary.Writer() - - data = text.encode() - data_size = len(data) - - writer.write_byte(0x01) - writer.write_int16(data_size + 3) - writer.write_byte(0x01) - writer.write_int16(data_size) - writer.write(data) - - return writer.clear() - - -def face(face_id: int) -> bytes: - writer = binary.Writer() - - writer.write_byte(0x02) - writer.write_int16(1 + 3) - writer.write_byte(0x01) - writer.write_int16(1) - writer.write_byte(face_id) - - return writer.clear() - - -async def at(user_id: int, group_id: int) -> bytes: - name = "@" + await net.get_user_cache( - user_id=user_id, - group_id=group_id - ) - - writer = binary.Writer() - writer.write_hex("00 01 00 00") - writer.write_int16(len(name)) - writer.write_hex("00") - writer.write_int32(user_id) - writer.write_hex("00 00") - data = writer.clear() - name = name.encode() - - writer.__init__() - writer.write_byte(0x01) - writer.write_int16(len(name)) - writer.write(name) - writer.write_byte(0x06) - writer.write_int16(len(data)) - writer.write(data) - data = writer.clear() - - writer.__init__() - writer.write_byte(0x01) - writer.write_int16(len(data)) - writer.write(data) - return writer.clear() + text(" ") - - -def xml(xml_code: str) -> bytes: - writer = binary.Writer() - xml_code = xml_code.replace("&", "&") - xml_code = xml_code.replace(",", ",") - data = zlib.compress(xml_code.encode(), -1) - - writer.write_byte(0x14) - writer.write_int16(len(data) + 11) - writer.write_hex("01") - writer.write_int16(len(data) + 1) - writer.write_hex("01") - writer.write(data) - writer.write_hex("02 00 04 00 00 00 02") - return writer.clear() - - -def music( - title: str = "", - content: str = "", - url: str = "", - audio: str = "", - cover: str = "" -) -> bytes: - xml_code = const.MUSIC_CODE.format( - title=title, - content=content, - url=url, - audio=audio, - cover=cover - ) - - return xml(xml_code) - -async def qqmusic(keyword: str): - info = json.loads((await cli.webget( - url="https://c.y.qq.com/soso/fcgi-bin/client_search_cp?w=" + - parse.quote(keyword) - )).content[9:-1])["data"]["song"]["list"][0] - - audio = (await cli.webget( - url="https://u.y.qq.com/cgi-bin/musicu.fcg?data=" + parse.quote( - json.dumps( - { - "comm": {"uin": 0, "format": "json", "ct": 24, "cv": 0}, - "req": {"module": "CDN.SrfCdnDispatchServer", "method": "GetCdnDispatch", "param": {"guid": "3982823384", "calltype": 0, "userip": ""}}, - "req_0": {"module": "vkey.GetVkeyServer", "method": "CgiGetVkey", "param": {"guid": "3982823384", "songmid": [info["songmid"]], "songtype": [0], "uin": "0", "loginflag": 1, "platform": "20"}} - } - )) - )).json()["req_0"]["data"]["midurlinfo"][0]["purl"] - - html = (await cli.webget(f"https://y.qq.com/n/ryqq/songDetail/{info['songmid']}")).content - start = html.find(b'photo_new\\u002F')+15 - end = html.find(b"?max_age", start) - - return music( - title=info["songname"], - content=info["singer"][0]["name"], - url=f"https://y.qq.com/n/yqq/song/{info['songmid']}.html", - audio="http://dl.stream.qqmusic.qq.com/" + audio, - cover="https://y.qq.com/music/photo_new/" + html[start:end].decode() - ) - - -class Image: - def __init__(self, im_data: bytes) -> None: - self.ukey = None - self.picid = None - self.finish = False - - self.data = im_data - self.size = len(im_data) - self.hash = utils.hashmd5(im_data) - self.width, self.height = utils.img_size(im_data) - self.uuid = "{%s}.jpg" % (str(uuid.UUID(bytes=self.hash)).upper()) - - -async def image_group(group_id: int, image: Image) -> bytes: - writer = binary.Writer() - await net.upload_group_image(group_id, image) - - wait_times = 90 - while not image.finish: - await asyncio.sleep(1) - wait_times -= 1 - if not wait_times: - raise Exception(f"获取图片 [PQ:image,file={image.hash.hex().upper()}] 超时") - - - - writer.write_hex("03 00 CB 02") - writer.write_int16(len(image.uuid)) - writer.write(image.uuid.encode()) - writer.write_hex("04 00 04") - - writer.write_hex("84 74 B1 53 05 00 04 BC") - writer.write_hex("EB 03 B7 06 00 04 00 00") - writer.write_hex("00 50 07 00 01 43 08 00") - writer.write_hex("00 09 00 01 01 0B 00 00") - writer.write_hex("14 00 04 11 00 00 00 15") - writer.write_hex("00 04 00 00 00 8B 16 00") - writer.write_hex("04 00 00 00 81 18 00 04") - writer.write_hex("00 00 0E D3 FF 00 5C 15") - writer.write_hex("36 20 39 32 6B 41 31 43") - writer.write_hex("38 34 37 34 62 31 35 33") - writer.write_hex("62 63 65 62 30 33 62 37") - writer.write_hex("20 20 20 20 20 20 35 30") - writer.write_hex("20 20 20 20 20 20 20 20") - writer.write_hex("20 20 20 20 20 20 20 20") - - writer.write(image.uuid.encode()) - writer.write_hex("41") - return writer.clear() - - -async def image_friend(user_id: int, image: Image) -> bytes: - writer = binary.Writer() - await net.upload_friend_image(user_id, image) - - wait_times = 90 - while not image.finish: - await asyncio.sleep(1) - wait_times -= 1 - if not wait_times: - raise Exception(f"获取图片 [PQ:image,file={image.hash.hex().upper()}] 超时") - - writer.write_hex("B6 E8 AF AD E4 BD 93 E7 AE 80 00 00 06 01 43 02") - writer.write_hex("00 1B") - writer.write((utils.randstr(23) + ".jpg").encode()) - writer.write_hex("03 00 04") - writer.write_int32(image.size) - writer.write_hex("04") - writer.write_int16(len(image.picid)) - writer.write(image.picid) - writer.write_hex("14 00 04 11 00 00 00 0B 00 00 18") - writer.write_int16(len(image.picid)) - writer.write(image.picid) - writer.write_hex("19 00 04 00 00") - writer.write_int16(image.width) - writer.write_hex("1A 00 04 00 00") - writer.write_int16(image.height) - writer.write_hex("1F 00 04 00") - - writer.write_hex("00 03 E8 1B 00 10") - writer.write(image.hash) - writer.write_hex("FF 00 75 16") - - writer.write(b' 117101061CB') - size = str(image.size).encode() - if len(size) > 5: - writer.write(b' ' * 4) - else: - writer.write(b' ' * 5) - - writer.write(size) - writer.write_byte(ord("e")) - writer.write((image.hash.hex().upper()+".jpg").encode()) - writer.write_byte(ord("x")) - writer.write(image.picid) - writer.write_byte(ord("A")) - return writer.clear() diff --git a/pcqq/network/__init__.py b/pcqq/network/__init__.py deleted file mode 100644 index aa781fb..0000000 --- a/pcqq/network/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from .pcapi import ( - send_friend_msg, - friend_receipt, - send_group_msg, - group_receipt, - upload_friend_image, - upload_group_image -) - -from .webapi import ( - get_group_info, - set_group_card, - set_group_shutup, - get_user_name, - get_user_cache, - get_group_cache, -) - -from .wtlogin import ( - set_online, - login_out, - heatbeat, - qrcode_login, - password_login, - token_login -) diff --git a/pcqq/network/pcapi.py b/pcqq/network/pcapi.py deleted file mode 100644 index cc05920..0000000 --- a/pcqq/network/pcapi.py +++ /dev/null @@ -1,293 +0,0 @@ -import time - -import pcqq.client as cli -import pcqq.utils as utils -import pcqq.const as const -import pcqq.logger as logger -import pcqq.binary as binary -#cli = cli.QQClient() - - -async def send_friend_msg(user_id: int, msg_data: bytes, has_image: bool = False): - """ - 发送好友消息 - - :param user_id: 好友QQ号 - - :param msg_data: PCQQ消息协议数据 - - """ - writer = binary.Writer() - time_stamp = int(time.time()).to_bytes(4, 'big')[::-1] - - writer.write_int32(cli.uin) - writer.write_int32(user_id) - writer.write_hex("00 00 00 08 00 01 00") - writer.write_hex("04 00 00 00 00 36 39") - writer.write_int32(cli.uin) - writer.write_int32(user_id) - - writer.write(utils.hashmd5(time_stamp)) - writer.write_hex("00 0B 37 96") - writer.write(time_stamp) - writer.write_hex("02 55 00 00 00 00 01 00 00 00") - writer.write_hex("0C 4D 53 47 00 00 00 00 00") - writer.write(time_stamp) - writer.write(time_stamp[::-1]) - - writer.write_hex("00 00 00 00 09 00 86 00 00") - if has_image: - writer.write_hex("12 E6 B1 89 E4 BB AA E8 9D") - else: - writer.write_hex("06 E5 AE 8B E4 BD 93 00 00") - writer.write(msg_data) - - await cli.write_packet( - "00 CD", - const.BODY_VERSION, - writer.clear() - ) - - -async def send_group_msg(group_id: int, msg_data: bytes, has_image: bool = False): - """ - 发送群消息 - - :param group_id: 目标群号 - - :param msg_data: PCQQ消息协议数据 - - :param has_image: 消息数据中是否含有图片数据 - - """ - writer = binary.Writer() - time_stamp = int(time.time()).to_bytes(4, 'big') - - if has_image: - writer.write_hex("00 02 01 00 00 00 00 00 00 00") - else: - writer.write_hex("00 01 01 00 00 00 00 00 00 00") - - writer.write_hex("4D 53 47 00 00 00 00 00") - writer.write(time_stamp) - writer.write(time_stamp[::-1]) - writer.write_hex("00 00 00 00 09 00 86 00 00") - writer.write_hex("06 E5 AE 8B E4 BD 93 00 00") - writer.write(msg_data) - data = writer.clear() - - writer.__init__() - writer.write_hex("2A") - writer.write_int32(utils.gid_from_group(group_id)) - writer.write_int16(len(data)) - writer.write(data) - - await cli.write_packet( - "00 02", - const.BODY_VERSION, - writer.clear() - ) - - -async def friend_receipt(user_id: int, timestamp: int): - writer = binary.Writer() - writer.write_hex("08 01") - writer.write_hex("12 03 98 01 00") - writer.write_hex("0A 0E 08") - writer.write_varint(user_id) - writer.write_hex("10") - writer.write_varint(timestamp) - writer.write_hex("20 00") - data = writer.clear() - - writer.__init__() - writer.write_hex("00 00 00 07") - writer.write_int32(len(data)-7) - writer.write(data) - - await cli.write_packet( - "03 19", - const.FUNC_VERSION, - writer.clear() - ) - - -async def group_receipt(group_id: int, msg_id: int): - writer = binary.Writer() - - writer.write_byte(41) - writer.write_int32(utils.gid_from_group(group_id)) - writer.write_byte(2) - writer.write_int32(msg_id) - - await cli.write_packet( - "00 02", - const.BODY_VERSION, - writer.clear() - ) - - -async def upload_group_image(group_id: int, image): - writer = binary.Writer() - - writer.write_pbint(group_id, 1) - writer.write_pbint(cli.uin, 2) - writer.write_pbint(0, 3) - writer.write_pbdata(image.hash, 4) - writer.write_pbint(image.size, 5) - writer.write_pbhex( - "37 00 4D 00 32 00 25 00 4C 00 31 00 56 00 32 00 7B 00 39 00 30 00 29 00 52 00", 6 - ) - writer.write_pbint(1, 7) - writer.write_pbint(1, 9) - writer.write_pbint(image.width, 10) - writer.write_pbint(image.height, 11) - writer.write_pbint(4, 12) - writer.write_pbdata("26656".encode(), 13) - data = writer.clear() - - writer.__init__() - writer.write_pbdata(data, 3) - data = writer.clear() - - writer.__init__() - writer.write_pbint(1, 19) - temp = writer.clear() - - writer.__init__() - writer.write_pbdata(temp, 2) - temp = writer.clear() - - writer.__init__() - writer.write_pbint(1, 1) - writer.write(temp) - writer.write_pbint(1, 2) - writer.write(data) - data = writer.clear() - - writer.__init__() - writer.write_hex("00 00 00 07 00 00") - writer.write_int16(len(data)) - writer.write(data) - writer.write_hex("70 00 78 03 80 01 00") - - sequence = utils.randbytes(2) - await cli.write_packet( - "03 88", - const.FUNC_VERSION, - writer.clear(), - sequence=sequence, - ) - - async def callback(body): - if len(body) < 128: - logger.info(f"图片获取成功 -> https://gchat.qpic.cn/gchatpic_new/0/0-0-{image.hash.hex().upper()}/0?term=3") - image.finish = True - return # 无需重新上传 - - start = body.find(bytes([66, 128, 1])) + 3 - end = body.find(bytes([128, 128, 8])) - 9 - image.ukey = body[start:end].hex().upper() - - await cli.webpost( - url="http://htdata2.qq.com/cgi-bin/httpconn?" + "&".join([ - "htcmd=0x6ff0071", - "ver=5515", - "term=pc", - f"ukey={image.ukey}", - f"filesize={image.size}", - "range=0", - f"uin={cli.uin}", - f"groupcode={group_id}" - ]), - data=image.data, - headers={"User-Agent": "QQClient"} - ) - logger.info(f"图片上传完成 -> https://gchat.qpic.cn/gchatpic_new/0/0-0-{image.hash.hex().upper()}/0?term=3") - image.finish = True - cli.waiter[sequence.hex()] = callback - - -async def upload_friend_image(user_id: int, image) -> bytes: - writer = binary.Writer() - - writer.write_pbint(cli.uin, 1) - writer.write_pbint(user_id, 2) - writer.write_pbint(0, 3) - writer.write_pbdata(image.hash, 4) - writer.write_pbint(image.size, 5) - writer.write_pbhex( - "45 00 4F 00 59 00 7B 00 56 00 29 00 45 00 56 00 4A 00 4B 00 48 00 48 00 53 00", 6 - ) - writer.write_pbint(1, 7) - writer.write_pbint(0, 9) - writer.write_pbint(image.width, 14) - writer.write_pbint(image.height, 15) - data = writer.clear() - - writer.__init__() - writer.write_pbint(1, 19) - writer.write_hex("98 03 F6 A8 BF A6 04 A0 03 01") - temp = writer.clear() - - writer.__init__() - writer.write_pbdata(data, 2) - data = writer.clear() - - writer.__init__() - writer.write_pbdata(temp, 2) - temp = writer.clear() - - writer.__init__() - writer.write_pbint(1, 1) - writer.write(temp) - writer.write_pbint(1, 1) - writer.write(data) - data = writer.clear() - - writer.__init__() - writer.write_hex("00 00 00 11 00 00") - writer.write_int16(len(data)-17) - writer.write(data) - - sequence = utils.randbytes(2) - await cli.write_packet( - "03 52", - const.FUNC_VERSION, - writer.clear(), - sequence=sequence, - ) - - async def callback(body): - reader = binary.Reader(body) - - if reader.tell() < 256: - reader.read(66) - image.picid = reader.read(55) # 无需重新上传 - logger.info(f"图片获取成功 -> https://c2cpicdw.qpic.cn/offpic_new/{cli.uin}/{image.picid.decode()}/0") - image.finish = True - return - - reader.read(70) - image.ukey = reader.read(336).hex().upper() - reader.read(2) - image.picid = reader.read(55) - - await cli.webpost( - url="http://htdata2.qq.com/cgi-bin/httpconn?" + "&".join([ - "htcmd=0x6ff0070", - "ver=5509", - f"ukey={image.ukey}", - f"filesize={image.size}", - "range=0", - f"uin={cli.uin}" - ]), - data=image.data, - headers={ - "User-Agent": "QQClient", - } - ) - logger.info(f"图片上传完成 -> https://c2cpicdw.qpic.cn/offpic_new/{cli.uin}/{image.picid.decode()}/0") - image.finish = True - - cli.waiter[sequence.hex()] = callback diff --git a/pcqq/network/webapi.py b/pcqq/network/webapi.py deleted file mode 100644 index 33e3c4e..0000000 --- a/pcqq/network/webapi.py +++ /dev/null @@ -1,208 +0,0 @@ -import json -import pcqq.utils as utils -import pcqq.client as cli -import pcqq.logger as logger - - -def write_ginfo(info: dict): - if not cli.cache.exist("ginfo", group_id=info["gid"]): - cli.cache.insert( - "ginfo", - info["name"], - info["owner"], - info["gid"], - info["board"], - info["intro"], - info["mem_num"], - ) - for mem in info["members"]: - if cli.cache.exist("gmember", user_id=mem["uid"], group_id=info["gid"]): - continue - cli.cache.insert( - "gmember", - mem["card"], - mem["uid"], - mem["name"], - info["gid"], - mem["op"] - ) - cli.cache.commit() - - -# 封装 Web Api 调用 -# cli = cli.QQClient() - -async def get_group_members(group_id: int) -> list: - """ - 获取群成员列表 - """ - rsp = await cli.webget( - "https://qinfo.clt.qq.com/cgi-bin/qun_info/get_group_members_new", - gc=group_id, - bkn=cli.bkn, - src="qinfo_v3", - headers={"Cookie": cli.cookie} - ) - rsp.content = rsp.content.split(b'\r\n')[1] - rsp.content = rsp.content.replace(b" ", b" ").replace(b"&", b"&") - ret = rsp.json() - - cards = ret.get("cards", {}) - admins = [ret["owner"], *ret.get("adm", [])] - return [ - { - "uid": mem["u"], - "name": mem["n"], - "card": cards.get(str(mem["u"]), mem["n"]), - "op": int(mem["u"] in admins) - } - for mem in ret["mems"] - ] - - -async def get_group_info(group_id: int) -> dict: - """ - 获取群信息 - """ - rsp = await cli.webget( - "https://qinfo.clt.qq.com/cgi-bin/qun_info/get_group_info_all", - gc=group_id, - bkn=cli.bkn, - src="qinfo_v3", - headers={"Cookie": cli.cookie} - ) - rsp.content = rsp.content.split(b'\r\n')[1] - rsp.content = rsp.content.replace(b" ", b" ").replace(b"&", b"&") - ret = rsp.json() - - return { - "gid": group_id, - "name": ret["gName"], - "owner": ret["gOwner"], - "board": ret["gBoard"], - "intro": ret["gRIntro"], - "mem_num": ret["gMemNum"], - "members": await get_group_members(group_id) - } - - -temp_cache = {} - - -async def get_user_name(user_id: int) -> str: - if user_id in temp_cache: - return temp_cache[user_id] - - rsp = await cli.webget("https://r.qzone.qq.com/fcg-bin/cgi_get_score.fcg", mask=7, uins=user_id) - rsp.content = rsp.content[17:-2] - temp_cache[user_id] = rsp.json()[str(user_id)][-4] - - return temp_cache[user_id] - - -async def set_group_card(group_id: int, user_id: int, nickname: str) -> None: - """ - 修改群成员卡片 - - :param group_id: 被修改成员所在的群号 - - :param user_id: 被修改成员的QQ号 - - :param nickname: 修改后的昵称 - - """ - rsp = await cli.webpost( - url="https://qinfo.clt.qq.com/cgi-bin/qun_info/set_group_card", - u=user_id, - name=nickname, - gc=group_id, - bkn=cli.bkn, - src="qinfo_v3", - headers={"Cookie": cli.cookie} - ) - rsp.content = rsp.content.split(b'\r\n')[1] - rsp.content = rsp.content.replace(b" ", b" ").replace(b"&", b"&") - ret = rsp.json() - - gname = await get_group_cache(group_id) - cord = await get_user_cache(user_id, group_id) - if ret["em"] == "ok": - logger.info("成功将 %s(%d) 在群聊 %s(%d) 的名片设置为 %s" % ( - cord, user_id, - gname, group_id, nickname - )) - else: - logger.error("无法在群聊 %s(%d) 中修改 %s(%d) 的名片" % ( - gname, group_id, - cord, user_id - )) - - -async def set_group_shutup(group_id: int, user_id: int, secs: int = 60): - """ - 设置群成员禁言 - - :param group_id: 被禁言成员所在的群号 - - :param user_id: 被禁言成员的QQ号 - - :param secs: 禁言时长(单位: 秒) - - """ - rsp = await cli.webget( - url="https://qinfo.clt.qq.com/cgi-bin/qun_info/set_group_shutup", - shutup_list=json.dumps([{"uin": user_id, "t": secs}]), - gc=group_id, - bkn=cli.bkn, - src="qinfo_v3", - headers={"Cookie": cli.cookie,} - ) - rsp.content = rsp.content.split(b'\r\n')[1] - rsp.content = rsp.content.replace(b" ", b" ").replace(b"&", b"&") - ret = rsp.json() - - gname = await get_group_cache(group_id) - cord = await get_user_cache(user_id, group_id) - if not ret["ec"]: - - logger.info("成功将 %s(%d) 在群聊 %s(%d) 中禁言, 预计解禁时间 %s" % ( - cord, user_id, - gname, group_id, - utils.time_lapse(secs) - )) - else: - logger.error("无法将 %s(%d) 在群聊 %s(%d) 中禁言" % ( - cord, user_id, - gname, group_id - )) - -# 封装 cache 简单调用 - - -async def get_user_cache(user_id: int, group_id: int = 0) -> tuple: - """ - 在缓存内取用户昵称 - """ - if group_id: - ret = cli.cache.select( - table_name="gmember", - user_id=user_id, - group_id=group_id - ) - if not ret: - write_ginfo(await get_group_info(group_id)) - return await get_user_cache(user_id, group_id) - return ret[0][0] - else: - return await get_user_name(user_id) - - -async def get_group_cache(group_id: int) -> str: - """ - 在缓存内取群名称 - """ - ret = cli.cache.select("ginfo", group_id=group_id) - if not ret: - write_ginfo(await get_group_info(group_id)) - return await get_group_cache(group_id) - return ret[0][0] diff --git a/pcqq/network/wtlogin.py b/pcqq/network/wtlogin.py deleted file mode 100644 index 30ecdc0..0000000 --- a/pcqq/network/wtlogin.py +++ /dev/null @@ -1,432 +0,0 @@ -import os -import asyncio -import platform -import subprocess - -import pcqq.client as cli -import pcqq.utils as utils -import pcqq.const as const -import pcqq.logger as logger -import pcqq.binary as binary -#cli = cli.QQClient() - - -async def ping_server(is_logging: bool): - """ - 0825包 一切的开始 均衡负载 - - 发这个包就问候服务器 Say Hello 判断是否能链接服务器 - """ - writer = binary.Writer() - - writer.write(binary.tlv018(cli.uin, cli.redirection_times)) - if is_logging: - writer.write(binary.tlv309( - cli.server_ip, - cli.redirection_history, - cli.redirection_times - )) - writer.write(binary.tlv036()) - else: - writer.write(binary.tlv004()) - writer.write(binary.tlv309( - cli.server_ip, - cli.redirection_history, - cli.redirection_times - )) - writer.write(binary.tlv114()) - - tea = binary.QQTea(bytes.fromhex(const.RANDKEY)) - await cli.write_packet( - "08 25", - const.STRUCT_VERSION, - bytes.fromhex(const.RANDKEY) + - tea.encrypt(writer.clear()) - ) - - _, _, _, body = await cli.read_packet() - reader = binary.Reader(tea.decrypt(body)) - - sign = reader.read_byte() - reader.read(2) - cli.token_0038_from_0825 = reader.read(reader.read_int16()) - reader.read(6) - cli.login_time = reader.read(4) - cli.local_ip = reader.read(4) - reader.read(2) - - if sign == 0xfe: - reader.read(18) - cli.server_ip = reader.read(4) - cli.redirection_times += 1 - cli.redirection_history += cli.server_ip - elif sign == 0x00: - reader.read(6) - cli.server_ip = reader.read(4) - cli.redirection_history = b'' - - -async def fetch_qrcode(): - """ - 0818包 向服务器申请登录二维码 - - 发这个包就代表需要使用手机扫描二维码登录 - """ - writer = binary.Writer() - - writer.write(binary.tlv019()) - writer.write(binary.tlv114()) - writer.write(binary.tlv305()) - - tea = binary.QQTea(bytes.fromhex(const.RANDKEY)) - await cli.write_packet( - "08 18", - const.STRUCT_VERSION, - bytes.fromhex(const.RANDKEY) + - tea.encrypt(writer.clear()) - ) - - _, _, _, body = await cli.read_packet() - tea.key = bytes.fromhex(const.SHAREKEY) - reader = binary.Reader(tea.decrypt(body)) - - if reader.read_byte() != 0x00: - logger.fatal("ERROR: 登录二维码获取失败,请尝试重新运行") - - reader.read(6) - cli.pckey_for_0819 = reader.read(16) - reader.read(4) - cli.token_0038_from_0818 = reader.read(reader.read_int16()) - reader.read(4) - cli.token_by_scancode = reader.read(reader.read_int16()) - reader.read(4) - - path = os.path.join(os.getcwd(), 'QrCode.jpg') - with open(path, mode="wb") as f: - f.write(reader.read(reader.read_int16())) - logger.info('登录二维码获取成功,已保存至' + path) - - system = platform.system() - try: - # 用系统默认程序打开图片文件 - if system == "Windows": - os.startfile(path) - elif system == "Linux": - subprocess.call(["xdg-open", path]) - else: - subprocess.call(["open", path]) - except: - # 使用pillow库在终端上打印二维码图片 - utils.print_qrcode(path, const.QRCODE_SIZE) - - -async def check_qrcode(): - """ - 0819包 检查当前扫码状态 - """ - writer = binary.Writer() - - writer.write(binary.tlv019()) - writer.write(binary.tlv114()) - writer.write_hex("03 01 00 22") - writer.write_int16(len(cli.token_by_scancode)) - writer.write(cli.token_by_scancode) - - tea = binary.QQTea(cli.pckey_for_0819) - await cli.write_packet( - "08 19", - const.STRUCT_VERSION + "00 30 00 3A", - len(cli.token_0038_from_0818).to_bytes(2, 'big') + - cli.token_0038_from_0818 + - tea.encrypt(writer.clear()) - ) - - _, _, uin, body = await cli.read_packet() - reader = binary.Reader(tea.decrypt(body)) - state = reader.read_byte() - - if state == 0x01: - logger.info(f'账号 {uin} 已扫码,请在手机上确认登录') - elif state == 0x00: - cli.uin = uin - - reader.read(2) - cli.password = reader.read(reader.read_int16()) - reader.read(2) - cli.tgt_key = reader.read(reader.read_int16()) - - os.remove('QrCode.jpg') - logger.info(f'账号 {cli.uin} 已在手机上确认登录,尝试登录中...') - return True - return False - - -async def check_login(): - """ - 0836包 登录验证/账密验证 - """ - writer = binary.Writer() - - writer.write(binary.tlv112(cli.token_0038_from_0825)) - writer.write(binary.tlv30f()) - writer.write(binary.tlv005(cli.uin)) - - if cli.is_scancode: - writer.write(binary.tlv303(cli.password)) - else: - writer.write(binary.tlv006( - uin=cli.uin, - tgt_key=cli.tgt_key, - md5_once=cli.password, - md5_twice=utils.hashmd5( - cli.password + - bytes(4) + - cli.uin.to_bytes(4, "big") - ), - login_time=cli.login_time, - local_ip=cli.local_ip, - computer_id=utils.hashmd5( - f'{cli.uin}ComputerID'.encode() - ), - tgtgt=cli.tgtgt_key - )) - - writer.write(binary.tlv015()) - writer.write(binary.tlv01a(cli.tgt_key)) - writer.write(binary.tlv018(cli.uin, cli.redirection_times)) - writer.write(binary.tlv103()) - writer.write(binary.tlv312()) - writer.write(binary.tlv508()) - writer.write(binary.tlv313()) - writer.write(binary.tlv102(cli.token_0038_from_0825)) - - tea = binary.QQTea(bytes.fromhex(const.SHAREKEY)) - await cli.write_packet( - "08 36", - const.STRUCT_VERSION + "00 01 01 02", - len(bytes.fromhex(const.PUBLICKEY)).to_bytes(2, 'big') + - bytes.fromhex(const.PUBLICKEY) + - bytes.fromhex("00 00 00 10") + - bytes.fromhex(const.RANDKEY) + - tea.encrypt(writer.clear()) - ) - - try: - _, _, _, body = await cli.read_packet() - except: - logger.fatal("ERROR: "+", ".join([ - "登录验证失败", - "可能是您的设备开启了登录保护", - "请在手机QQ的[设置]->[账号安全]->[登录设备管理]中关闭[登录保护]选项" - ])) - - try: - body = tea.decrypt(body) - tea.key = cli.tgt_key - reader = binary.Reader(tea.decrypt(body)) - except: - logger.fatal("ERROR: 登录失败,请尝试重启程序或使用扫码登录") - - sign = reader.read_byte() - if sign == 0x00: - for _ in range(5): - reader.read_byte() - - binary.tlv_id = reader.read_byte() - binary.tlv_size = reader.read_int16() - - if binary.tlv_id == 9: - reader.read(2) - cli.pckey_for_0828_send = reader.read(16) - - cli.token_0038_from_0836 = reader.read( - reader.read_int16()) - - reader.read(reader.read_int16()) - reader.read(2) - elif binary.tlv_id == 8: - reader.read(8) - cli.nickname = reader.read(reader.read_byte()).decode() - elif binary.tlv_id == 7: - reader.read(26) - cli.pckey_for_0828_recv = reader.read(16) - - cli.token_0088_from_0836 = reader.read( - reader.read_int16()) - reader.read(binary.tlv_size - 180) - else: - reader.read(binary.tlv_size) - elif sign == 0x01: - reader.read(2) - cli.tgt_key = reader.read(reader.read_int16()) - reader.read(2) - cli.tgtgt_key = reader.read(reader.read_int16()) - - tea.key = utils.hashmd5( - cli.password + - bytes(4) + - cli.uin.to_bytes(4, "big") - ) - cli.tgtgt_key = tea.decrypt(cli.tgtgt_key) - - return await check_login() - else: - logger.fatal(f"ERROR: 登录失败,错误码{hex(sign)},请尝试重新运行或使用扫码登录") - - -async def fetch_session(): - """ - 0828包 申请SessionKey - 此包代表获取会话密钥来操作协议功能 - """ - writer = binary.Writer() - - writer.write(binary.tlv007(cli.token_0088_from_0836)) - writer.write(binary.tlv00c(cli.server_ip)) - writer.write(binary.tlv015()) - writer.write(binary.tlv036()) - writer.write(binary.tlv018(cli.uin, cli.redirection_times)) - writer.write(binary.tlv01f()) - writer.write(binary.tlv105()) - writer.write(binary.tlv10b()) - writer.write(binary.tlv02d(cli.local_ip)) - - tea = binary.QQTea(cli.pckey_for_0828_send) - await cli.write_packet( - "08 28", - const.BODY_VERSION + "00 30 00 3A 00 38", - cli.token_0038_from_0836 + - tea.encrypt(writer.clear()) - ) - - _, _, _, body = await cli.read_packet() - tea.key = cli.pckey_for_0828_recv - reader = binary.Reader(tea.decrypt(body)) - reader.read(63) - cli.session_key = reader.read(16) - cli.teaer = binary.QQTea(cli.session_key) - - -async def set_online(state: int): - """00EC包 设置QQ状态""" - writer = binary.Writer() - - writer.write_hex("01 00") - writer.write_byte(state) - writer.write_hex("00 01") - writer.write_hex("00 01") - writer.write_hex("00 04") - writer.write_hex("00 00 00 00") - - await cli.write_packet( - "00 EC", - const.BODY_VERSION, - writer.clear() - ) - await cli.read_packet() - logger.info(f"账号 {cli.uin} 已上线,欢迎用户 {cli.nickname} 使用本协议库") - - -async def login_out(): - """0062包 注销当前登录""" - await cli.write_packet( - "00 62", - const.BODY_VERSION, - bytes(16) - ) - logger.info(f"账号 {cli.uin} 已退出登录,欢迎用户 {cli.nickname} 下次使用本协议库") - - -async def heatbeat(): - """0058包 与服务端保持心跳""" - await cli.write_packet( - "00 58", - const.BODY_VERSION, - bytes.fromhex("00 01 00 01") - ) - - -async def fetch_skey(): - """001D包 获取网络操作用的skey""" - writer = binary.Writer() - - writer.write_byte(51) - writer.write_int16(6) # 域名数量 - - writer.write_int16(8) - writer.write(b't.qq.com') - - writer.write_int16(10) - writer.write(b'qun.qq.com') - - writer.write_int16(12) - writer.write(b'qzone.qq.com') - - writer.write_int16(12) - writer.write(b'jubao.qq.com') - - writer.write_int16(9) - writer.write(b'ke.qq.com') - - writer.write_int16(10) - writer.write(b'tenpay.com') - - await cli.write_packet( - "00 1D", - const.BODY_VERSION, - writer.clear() - ) - - _, _, _, body = await cli.read_packet() - reader = binary.Reader(body) - - reader.read(2) - cli.skey = reader.read(reader.read_int16()).decode() - cli.bkn = utils.gtk_skey(cli.skey) - cli.cookie = "uin=o%d; skey=%s" % (cli.uin, cli.skey) - -async def qrcode_login(): - """通过手机扫码登录QQ""" - cli.is_scancode = True - await ping_server(False) # 请求登录服务器 - await fetch_qrcode() # 获取登录二维码 - - # 循环检测扫码状态 - while not await check_qrcode(): - await asyncio.sleep(1.0) - - await check_login() # 验证登录 - await fetch_session() # 申请会话操作密钥 - await set_online(const.STATE_ONLINE) # 设置状态为在线 - await fetch_skey() # 申请网络操作密钥 - - cli.save_token(os.path.join(os.getcwd(), "session.token")) - -async def password_login(user_id:int, password:str): - """通过账号密码登录QQ""" - cli.uin = user_id - cli.password = utils.hashmd5(password.encode()) - - await ping_server(False) # 请求登录服务器 - await check_login() # 账密登录 - await fetch_session() # 申请会话操作密钥 - await set_online(const.STATE_ONLINE) # 设置状态为在线 - await fetch_skey() # 申请网络操作密钥 - - cli.save_token(os.path.join(os.getcwd(), "session.token")) - -async def token_login(): - """通过本地token申请重连""" - await ping_server(False) # 请求登录服务器 - - cli.load_token(os.path.join(os.getcwd(), "session.token")) - await ping_server(False) # 重新请求登录服务器 - - try: - await fetch_session() # 验证登录 - except: - os.remove(os.path.join(os.getcwd(), "session.token")) - logger.fatal("ERROR: 本地Token已失效,请重新登录") - - await set_online(const.STATE_ONLINE) # 设置状态为在线 - await fetch_skey() # 申请网络操作密钥 \ No newline at end of file diff --git a/pcqq/plugin.py b/pcqq/plugin.py deleted file mode 100644 index 96e619d..0000000 --- a/pcqq/plugin.py +++ /dev/null @@ -1,173 +0,0 @@ -import re -import asyncio -import pcqq.client as cli -import pcqq.logger as logger - - -class BasePlugin: - def __init__(self, rules): - self.rules = rules - self.temp = ... - self.block = ... - self.handle = ... - self.priority = ... - - def __call__(self, handle): - self.handle = handle - return self - - def SetTemp(self, temp: bool): - self.temp = temp - return self - - def SetBlock(self, block: bool): - self.block = block - return self - - def SetPriority(self, priority=10): - self.priority = priority - return self - -# Builtin Plugin Rule - - -async def only_group(session): - return bool(session.group_id) - - -async def only_friend(session): - return not session.group_id and session.user_id - - -def check_at(user_id): - async def check_rule(session): - return {"type": "at", "qq": user_id} in session.raw_message - return check_rule - - -def check_type(*type_group): - async def check_rule(session): - return session.event_type in type_group - return check_rule - - -def check_user(*uid_group): - async def check_rule(session): - return session.user_id in uid_group - return check_rule - - -def check_group(*gid_group): - async def check_rule(session): - return session.group_id in gid_group - return check_rule - - -def check_session(session_): - async def check_rule(session): - user_eq = session_.user_id == session.user_id - group_eq = session_.group_id == session.group_id - time_eq = session_.timestamp < session.timestamp - return user_eq and group_eq and time_eq - return check_rule - - -def must_given(prompt): - async def wait_rule(session): - await session.send_msg({"type": "at", "qq": session.user_id}, prompt) - - @on(check_session(session), temp=True, block=True, priority=-float("inf")) - async def wait_input(next_session): - session.matched = next_session.message - - wait_times = 60 - while not session.matched and wait_times: - await asyncio.sleep(0.5) - wait_times -= 1 - - try: - del cli.plugins[cli.plugins.index(wait_input)] - logger.error(f"等待会话 {session.user_id} 输入 -> 超时") - except ValueError: - logger.info(f"捕获会话 {session.user_id} 输入 -> {session.matched}") - - return bool(session.matched) - return wait_rule - -# Builtin Plugin registrar - - -def on(*rules, temp=False, block=False, priority=10): - plugin = BasePlugin(rules) - plugin.SetTemp(temp) - plugin.SetBlock(block) - plugin.SetPriority(priority) - - cli.plugins.append(plugin) - cli.plugins.sort(key=lambda p: p.priority) - return plugin - - -def on_full(key, *rules, **params): - async def full_rule(session): - return key == session.message - return on(*rules, full_rule, **params) - - -def on_fulls(key_group, *rules, **params): - async def fulls_rule(session): - return session.message in key_group - return on(*rules, fulls_rule, **params) - - -def on_keyword(key, *rules, **params): - async def key_rule(session): - return key in session.message - return on(*rules, key_rule, **params) - - -def on_keywords(key_group, *rules, **params): - async def keys_rule(session): - for key in key_group: - if key in session.message: - return True - return False - return on(*rules, keys_rule, **params) - - -def on_command(cmd, *rules, prompt="", **params): - async def cmd_rule(session): - if session.message.startswith(cmd): - session.matched = session.message[len(cmd):].strip() - - if session.matched == "" and prompt: - return await must_given(prompt)(session) - - return bool(session.matched) - - return on(*rules, cmd_rule, **params) - - -def on_commands(cmd_group, *rules, prompt="", **params): - async def cmds_rule(session): - if session.matched: - return True - - for cmd in cmd_group: - if session.message.startswith(cmd): - session.matched = session.message[len(cmd):].strip() - - if session.matched == "" and prompt: - return await must_given(prompt)(session) - - return bool(session.matched) - - return on(*rules, cmds_rule, **params) - - -def on_regex(pattern, *rules, **params): - async def reg_rule(session): - session.matched = re.findall(pattern, session.message) - - return bool(session.matched) - return on(*rules, reg_rule, **params) diff --git a/pcqq/plugin.pyi b/pcqq/plugin.pyi deleted file mode 100644 index 74e9b24..0000000 --- a/pcqq/plugin.pyi +++ /dev/null @@ -1,290 +0,0 @@ -import nonebot -from .session import Session -from typing import Any, Tuple, Callable, Iterable, Coroutine - - -class BasePlugin: - temp: bool - block: bool - priority: int - - session: Session - rules: Tuple[Callable[[Session], Coroutine[Any, Any, bool]]] - handle: Callable[[Session], Coroutine[Any, Any, None]] - - def __init__(self, - rules: Tuple[Callable[[Session], Coroutine[Any, Any, bool]]] - ) -> None: ... - - def __call__(self, - handle: Callable[[Session], Coroutine[Any, Any, None]] - ) -> BasePlugin: ... - - def SetTemp(self, temp: bool) -> BasePlugin: - """ - 设置当前插件是否为临时插件 - - 临时插件匹配成功一次后会自动删除 - """ - - def SetBlock(self, block: bool) -> BasePlugin: - """ - 设置当前插件处理成功后是否阻断后续插件执行 - """ - - def SetPriority(self, priority: bool) -> BasePlugin: - """ - 设置当前插件优先级 - - 优先级默认为 10, priority的数值越小优先级越高 - """ - -async def only_group(session:Session)->bool: - """ - 判断该会话是否为群事件 - """ - - -async def only_friend(session:Session)->bool: - """ - 判断该会话是否为好友事件 - """ - - -def check_at(user_id:int) -> Callable[[Session], Coroutine[Any, Any, bool]]: - """ - 判断用户是否在此次事件中被 At - - :param user_id: 被判断用户的 QQ 账号 - """ - - -def check_type(*type_group:str) -> Callable[[Session], Coroutine[Any, Any, bool]]: - """ - 判断事件类型 - - :param type_group: 用于判断的事件类型组 - - 群消息 group_msg - - 好友消息 friend_msg - - 进群事件 group_increase - - 退群事件 group_decrease - - 禁言事件 group_shutup - - """ - - -def check_user(*uid_group:Tuple[int]) -> Callable[[Session], Coroutine[Any, Any, bool]]: - """ - 判断事件来源的用户 - - :param uid_group: 用于判断的账号元组(以可变参传入) - """ - - -def check_group(*gid_group:Tuple[int]) -> Callable[[Session], Coroutine[Any, Any, bool]]: - """ - 判断事件来源的群 - - :param gid_group: 用于判断的群号元组(以可变参传入) - """ - -def check_session(session_: Session) -> Callable[[Session], Coroutine[Any, Any, bool]]: - """ - 判断两次 Session 的连续性 - - :param session_: 任意一次 Session - """ - -def must_given(prompt: str) -> Callable[[Session], Coroutine[Any, Any, bool]]: - """ - 将事件来源 30 秒内的下一次输入当作 session.matched - - :param prompt: 发送给消息来源索要参数的语句 - """ - -def on( - *rules: Callable[[Session], Coroutine[Any, Any, bool]], - temp: bool = False, - block: bool = False, - priority: int = 10 -) -> BasePlugin: - """ - 元匹配触发器 - - :param rules: 匹配规则函数集 - - :param temp: 是否为临时插件 - - :param block: 执行完成是否阻断后续插件 - - :param priority: 插件优先等级 - """ - - -def on_full( - key: str, - *rules: Callable[[Session], Coroutine[Any, Any, bool]], - temp: bool = False, - block: bool = False, - priority: int = 10 -) -> BasePlugin: - """ - 当消息内容为关键词时满足匹配 - - :param key: 关键词 - - :param rules: 匹配规则函数集 - - :param temp: 是否为临时插件 - - :param block: 执行完成是否阻断后续插件 - - :param priority: 插件优先等级 - """ - - -def on_fulls( - key_group: Iterable[str], - *rules: Callable[[Session], Coroutine[Any, Any, bool]], - temp: bool = False, - block: bool = False, - priority: int = 10 -) -> BasePlugin: - """ - 当消息内容是关键词组中任意关键词时满足匹配 - - :param key_group: 关键词组 - - :param rules: 匹配规则函数集 - - :param temp: 是否为临时插件 - - :param block: 执行完成是否阻断后续插件 - - :param priority: 插件优先等级 - """ - - -def on_keyword( - key: str, - *rules: Callable[[Session], Coroutine[Any, Any, bool]], - temp: bool = False, - block: bool = False, - priority: int = 10 -) -> BasePlugin: - """ - 当消息内容中含有关键词时满足匹配 - - :param key: 关键词 - - :param rules: 匹配规则函数集 - - :param temp: 是否为临时插件 - - :param block: 执行完成是否阻断后续插件 - - :param priority: 插件优先等级 - """ - - -def on_keywords( - key_group: Iterable[str], - *rules: Callable[[Session], Coroutine[Any, Any, bool]], - temp: bool = False, - block: bool = False, - priority: int = 10 -) -> BasePlugin: - """ - 当消息内容中含有关键词组中任意关键词时满足匹配 - - :param key_group: 关键词组 - - :param rules: 匹配规则函数集 - - :param temp: 是否为临时插件 - - :param block: 执行完成是否阻断后续插件 - - :param priority: 插件优先等级 - """ - - -def on_command( - cmd: str, - *rules: Callable[[Session], Coroutine[Any, Any, bool]], - prompt:str = "", - temp: bool = False, - block: bool = False, - priority: int = 10 -) -> BasePlugin: - """ - 当命令词在消息内容首部时满足匹配 - - 匹配结果保存至 session.match - - :param cmd: 命令词 - - :param rules: 匹配规则函数集 - - :param temp: 是否为临时插件 - - :param block: 执行完成是否阻断后续插件 - - :param priority: 插件优先等级 - - :param prompt: 若匹配内容为空, 则发送 prompt 语句, 并将对方的下一条消息当作匹配结果 - """ - -def on_commands( - cmd_group: Tuple[str], - *rules: Callable[[Session], Coroutine[Any, Any, bool]], - prompt:str = "", - temp: bool = False, - block: bool = False, - priority: int = 10 -) -> BasePlugin: - """ - 当命令词组内任意目录在消息内容首部时满足匹配 - - 匹配结果保存至 session.match - - :param cmd_group: 命令词组 - - :param rules: 匹配规则函数集 - - :param temp: 是否为临时插件 - - :param block: 执行完成是否阻断后续插件 - - :param priority: 插件优先等级 - - :param prompt: 若匹配内容为空, 则发送 prompt 语句, 并将对方的下一条消息当作匹配结果 - """ - -def on_regex( - pattern:str, - *rules: Callable[[Session], Coroutine[Any, Any, bool]], - temp: bool = False, - block: bool = False, - priority: int = 10 -) -> BasePlugin: - """ - 当消息内容能被此正则式解析是满足匹配 - - 匹配结果保存至 session.match - - :param pattern: 正则表达式 - - :param rules: 匹配规则函数集 - - :param temp: 是否为临时插件 - - :param block: 执行完成是否阻断后续插件 - - :param priority: 插件优先等级 - """ \ No newline at end of file diff --git a/pcqq/session.py b/pcqq/session.py deleted file mode 100644 index 225e668..0000000 --- a/pcqq/session.py +++ /dev/null @@ -1,115 +0,0 @@ -import pcqq.client as cli -import pcqq.network as net -import pcqq.message as msg -import pcqq.logger as logger -# cli = cli.QQClient() - - -class Session: - msg_id: int = 0 - timestamp: int = 0 - - self_id: int = 0 - user_id: int = 0 - group_id: int = 0 - target_id: int = 0 - - matched = None - message: str = "" - event_type: str = "" - raw_message: list = [] - - async def send_msg(self, *message): - if self.group_id: - await self.send_group_msg(self.group_id, *message) - else: - await self.send_friend_msg(self.user_id, *message) - - async def send_group_msg(self, group_id, *message): - data = b"" - escape = "" - has_image = False - - for seg in message: - if isinstance(seg, str): - escape += seg - data += msg.text(seg) - - if isinstance(seg, dict) and "type" in seg: - if seg["type"] == "at" and "qq" in seg: - data += await msg.at(int(seg["qq"]), group_id) - escape += f"[PQ:at,qq={seg['qq']}]" - - elif seg["type"] == "face" and "id" in seg: - data += msg.face(int(seg["id"])) - escape += f"[PQ:face,qq={seg['id']}]" - elif seg["type"] == "xml" and "code" in seg: - data += msg.xml(seg["code"]) - escape += f"[PQ:xml,code={seg['code']}]" - elif seg["type"] == "music": - if "keyword" in seg: - data += await msg.qqmusic(str(seg["keyword"])) - escape += f"[PQ:music,keyword={seg['keyword']}]" - else: - del seg["type"] - data += msg.music(**seg) - escape += f"[PQ:music,{','.join([k+'='+v for k,v in seg.items()])}]" - elif seg["type"] == "image": - if "data" in seg and isinstance(seg["data"], bytes): - image = msg.Image(seg["data"]) - - elif "url" in seg and seg["url"].startswith("http"): - image = msg.Image((await cli.webget(seg["url"])).content) - else: - continue - - data += await msg.image_group(group_id, image) - escape += f"[PQ:image,file={image.hash.hex().upper()}]" - has_image = True - - await net.send_group_msg(group_id, data, has_image) - logger.info(f"发送群消息: %s -> %s(%d)" % ( - escape, await net.get_group_cache(group_id), group_id - )) - - async def send_friend_msg(self, user_id, *message): - data = b"" - escape = "" - has_image = False - for seg in message: - if isinstance(seg, str): - escape += seg - data += msg.text(seg) - - if isinstance(seg, dict) and "type" in seg: - if seg["type"] == "face" and "id" in seg: - data += msg.face(int(seg["id"])) - escape += f"[PQ:face,qq={seg['id']}]" - elif seg["type"] == "xml" and "code" in seg: - data += msg.xml(seg["code"]) - escape += f"[PQ:xml,code={seg['code']}]" - elif seg["type"] == "music": - if "keyword" in seg: - data += await msg.qqmusic(str(seg["keyword"])) - escape += f"[PQ:music,keyword={seg['keyword']}]" - else: - del seg["type"] - data += msg.music(**seg) - escape += f"[PQ:music,{','.join([k+'='+v for k,v in seg.items()])}]" - elif seg["type"] == "image": - if "data" in seg and isinstance(seg["data"], bytes): - image = msg.Image(seg["data"]) - - elif "url" in seg and seg["url"].startswith("http"): - image = msg.Image((await cli.webget(seg["url"])).content) - else: - continue - - data += await msg.image_friend(user_id, image) - escape += f"[PQ:image,file={image.hash.hex().upper()}]" - has_image = True - - await net.send_friend_msg(user_id, data, has_image) - logger.info(f"发送好友消息: %s -> %s(%d)" % ( - escape, await net.get_user_cache(user_id), user_id - )) diff --git a/pcqq/session.pyi b/pcqq/session.pyi deleted file mode 100644 index af023bd..0000000 --- a/pcqq/session.pyi +++ /dev/null @@ -1,83 +0,0 @@ -from typing import Any, List, Dict, Union - - -class Session: - msg_id: int - timestamp: int - - self_id: int - user_id: int - group_id: int - target_id: int - - matched: Any - event_type: str - message: str - raw_message: List[Union[str, Dict[str, Any]]] - - async def send_msg(self, *message: Union[str, Dict[str, Any]]): - """ - 向事件来源发送消息, 发送文本消息直接传入字符串即可,特殊消息则需要使用 Dict 来表示 - - :param message: 发送的消息(消息段以可变参数的形式传入) - - At -> {"type": "at", "qq": 被At人的账号} - - 表情 -> {"type": "face", "id": 表情ID} - - 卡片 -> {"type": "xml", "code": XML卡片代码} - - 图片 -> {"type": "image", "data": 图片数据} - - 网络图片 -> {"type": "image", "url": 图片直链} - - QQ音乐 -> {"type": "music", "keyword": 搜索关键词} - - 自定义音乐 -> {"type": "music", "title": 音乐标题, "content": 音乐作者, "url": 跳转链接, "audio": 音频直链, "cover": 封面直链} - - """ - async def send_group_msg(self, group_id: int, *message: Union[str, Dict[str, Any]]): - """ - 发送群消息, 发送文本消息直接传入字符串即可,特殊消息则需要使用 Dict 来表示 - - :param group_id: 指定群号 - - :param message: 发送的消息(消息段以可变参数的形式传入) - - At -> {"type": "at", "qq": 被At人的账号} - - 表情 -> {"type": "face", "id": 表情ID} - - 卡片 -> {"type": "xml", "code": XML卡片代码} - - 图片 -> {"type": "image", "data": 图片数据} - - 网络图片 -> {"type": "image", "url": 图片直链} - - QQ音乐 -> {"type": "music", "keyword": 搜索关键词} - - 自定义音乐 -> {"type": "music", "title": 音乐标题, "content": 音乐作者, "url": 跳转链接, "audio": 音频直链, "cover": 封面直链} - - """ - - async def send_friend_msg(self, user_id: int, *message: Union[str, Dict[str, Any]]): - """ - 发送好友消息, 发送文本消息直接传入字符串即可,特殊消息则需要使用 Dict 来表示 - - :param group_id: 好友账号 - - :param message: 发送的消息(消息段以可变参数的形式传入) - - 表情 -> {"type": "face", "id": 表情ID} - - 卡片 -> {"type": "xml", "code": XML卡片代码} - - 图片 -> {"type": "image", "data": 图片数据} - - 网络图片 -> {"type": "image", "url": 图片直链} - - QQ音乐 -> {"type": "music", "keyword": 搜索关键词} - - 自定义音乐 -> {"type": "music", "title": 音乐标题, "content": 音乐作者, "url": 跳转链接, "audio": 音频直链, "cover": 封面直链} - - """ diff --git a/pcqq/utils/__init__.py b/pcqq/utils/__init__.py deleted file mode 100644 index 686a859..0000000 --- a/pcqq/utils/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from .sqlite import SqliteDB -from .imsize import img_size -from .qrcode import print_qrcode -from .group import ( - gid_from_group, - group_from_gid -) -from .helper import ( - hashmd5, - randstr, - randbytes, - gtk_skey, - time_lapse -) diff --git a/pcqq/utils/group.py b/pcqq/utils/group.py deleted file mode 100644 index 6678af0..0000000 --- a/pcqq/utils/group.py +++ /dev/null @@ -1,83 +0,0 @@ -def gid_from_group(group_id: int) -> int: - group = str(group_id) - left = int(group[0:-6]) - if left >= 0 and left <= 10: - right = group[-6:] - gid = str(left + 202) + right - elif left >= 11 and left <= 19: - right = group[-6:] - gid = str(left + 469) + right - elif left >= 20 and left <= 66: - left = int(str(left)[0:1]) - right = group[-7:] - gid = str(left + 208) + right - elif left >= 67 and left <= 156: - right = group[-6:] - gid = str(left + 1943) + right - elif left >= 157 and left <= 209: - left = int(str(left)[0:2]) - right = group[-7:] - gid = str(left + 199) + right - elif left >= 210 and left <= 309: - left = int(str(left)[0:2]) - right = group[-7:] - gid = str(left + 389) + right - elif left >= 310 and left <= 335: - left = int(str(left)[0:2]) - right = group[-7:] - gid = str(left + 349) + right - elif left >= 336 and left <= 386: - left = int(str(left)[0:3]) - right = group[-6:] - gid = str(left + 2265) + right - elif left >= 387 and left <= 499: - left = int(str(left)[0:3]) - right = group[-6:] - gid = str(left + 3490) + right - elif left >= 500: - return int(group) - return int(gid) - - -def group_from_gid(gid: int) -> int: - gid = str(gid) - if int(gid[0:3]) >= 500: - return int(gid) - left = int(gid[0:-6]) - - if left == 202: - right = gid[-6:] - group = int(right) - elif left >= 203 and left <= 212: - right = gid[-6:] - group = int(str(left - 202) + right) - elif left >= 480 and left <= 488: - right = gid[-6:] - group = int(str(left - 469) + right) - elif left >= 2010 and left <= 2099: - right = gid[-6:] - group = int(str(left - 1943) + right) - elif left >= 2100 and left <= 2146: - left = int(str(left)[0:3]) - right = gid[-7:] - group = int(str(left - 208) + right) - elif left >= 2147 and left <= 2199: - left = int(str(left)[0:3]) - right = gid[-7:] - group = int(str(left - 199) + right) - elif left >= 2601 and left <= 2651: - left = int(str(left)[0:4]) - right = gid[-6:] - group = int(str(left - 2265) + right) - elif left >= 3800 and left <= 3989: - left = int(str(left)[0:3]) - right = gid[-7:] - group = int(str(left - 349) + right) - elif left >= 4100 and left <= 4199: - left = int(str(left)[0:3]) - right = gid[-7:] - group = int(str(left - 389) + right) - else: - group = 0 - - return group \ No newline at end of file diff --git a/pcqq/utils/helper.py b/pcqq/utils/helper.py deleted file mode 100644 index b671c69..0000000 --- a/pcqq/utils/helper.py +++ /dev/null @@ -1,25 +0,0 @@ -import time -import random -import string -import hashlib - -def hashmd5(src: bytes): - return hashlib.md5(src).digest() - -def randbytes(size: int): - return bytes([random.randint(0, 255) for _ in range(size)]) - -def randstr(size: int): - return ''.join(random.sample(string.ascii_letters + string.digits, size)) - - -def gtk_skey(skey: str) -> int: - accu = 5381 - for s in skey: - accu += (accu << 5) + ord(s) - return accu & 2147483647 - - -def time_lapse(seconds: int): - time_stamp = int(time.time()) + seconds - return time.strftime("%Y年%m月%d日 %H:%M:%S", time.localtime(time_stamp)) \ No newline at end of file diff --git a/pcqq/utils/imsize.py b/pcqq/utils/imsize.py deleted file mode 100644 index 8d7a4b7..0000000 --- a/pcqq/utils/imsize.py +++ /dev/null @@ -1,144 +0,0 @@ -import re -import io -import struct -from xml.etree import ElementTree - - -def _convert_topx(value): - matched = re.match(r"(\d+)(?:\.\d)?([a-z]*)$", value) - if not matched: - raise ValueError("unknown length value: %s" % value) - else: - length, unit = matched.groups() - if unit == "": - return int(length) - elif unit == "cm": - return int(length) * 96 / 2.54 - elif unit == "mm": - return int(length) * 96 / 2.54 / 10 - elif unit == "in": - return int(length) * 96 - elif unit == "pc": - return int(length) * 96 / 6 - elif unit == "pt": - return int(length) * 96 / 6 - elif unit == "px": - return int(length) - else: - raise ValueError("unknown unit type: %s" % unit) - - -def img_size(im_data: bytes): - """ - Return (width, height) for a given img file content - no requirements - :type filepath: Union[str, pathlib.Path] - :rtype Tuple[int, int] - """ - height = -1 - width = -1 - - with io.BytesIO(im_data) as fhandle: - head = fhandle.read(24) - size = len(head) - # handle GIFs - if size >= 10 and head[:6] in (b'GIF87a', b'GIF89a'): - # Check to see if content_type is correct - try: - width, height = struct.unpack("= 24 and head.startswith(b'\211PNG\r\n\032\n') and head[12:16] == b'IHDR': - try: - width, height = struct.unpack(">LL", head[16:24]) - except struct.error: - raise ValueError("Invalid PNG file") - # Maybe this is for an older PNG version. - elif size >= 16 and head.startswith(b'\211PNG\r\n\032\n'): - # Check to see if we have the right content type - try: - width, height = struct.unpack(">LL", head[8:16]) - except struct.error: - raise ValueError("Invalid PNG file") - # handle JPEGs - elif size >= 2 and head.startswith(b'\377\330'): - try: - fhandle.seek(0) # Read 0xff next - size = 2 - ftype = 0 - while not 0xc0 <= ftype <= 0xcf or ftype in [0xc4, 0xc8, 0xcc]: - fhandle.seek(size, 1) - byte = fhandle.read(1) - while ord(byte) == 0xff: - byte = fhandle.read(1) - ftype = ord(byte) - size = struct.unpack('>H', fhandle.read(2))[0] - 2 - # We are at a SOFn block - fhandle.seek(1, 1) # Skip `precision' byte. - height, width = struct.unpack('>HH', fhandle.read(4)) - except struct.error: - raise ValueError("Invalid JPEG file") - # handle JPEG2000s - elif size >= 12 and head.startswith(b'\x00\x00\x00\x0cjP \r\n\x87\n'): - fhandle.seek(48) - try: - height, width = struct.unpack('>LL', fhandle.read(8)) - except struct.error: - raise ValueError("Invalid JPEG2000 file") - # handle big endian TIFF - elif size >= 8 and head.startswith(b"\x4d\x4d\x00\x2a"): - offset = struct.unpack('>L', head[4:8])[0] - fhandle.seek(offset) - ifdsize = struct.unpack(">H", fhandle.read(2))[0] - for i in range(ifdsize): - tag, datatype, count, data = struct.unpack( - ">HHLL", fhandle.read(12)) - if tag == 256: - if datatype == 3: - width = int(data / 65536) - elif datatype == 4: - width = data - else: - raise ValueError( - "Invalid TIFF file: width column data type should be SHORT/LONG.") - elif tag == 257: - if datatype == 3: - height = int(data / 65536) - elif datatype == 4: - height = data - else: - raise ValueError( - "Invalid TIFF file: height column data type should be SHORT/LONG.") - if width != -1 and height != -1: - break - if width == -1 or height == -1: - raise ValueError( - "Invalid TIFF file: width and/or height IDS entries are missing.") - elif size >= 8 and head.startswith(b"\x49\x49\x2a\x00"): - offset = struct.unpack('= 5 and head.startswith(b' None: - db = sqlite3.connect(path) - cu = db.cursor() - - self.close = db.close - self.commit = db.commit - self.execute = cu.execute - self.fetchone = cu.fetchone - self.fetchall = cu.fetchall - - def create(self, table_name: str, *args) -> bool: - try: - self.execute("create table %s %s" % ( - table_name, str(args).replace("'", "") - )) - self.commit() - return True - except: - return False - - def insert(self, table_name: str, *args): - '''调用本方法后需手动调用commit方法''' - args = [f"'{arg}'" if isinstance(arg, str) else str(arg) for arg in args] - self.execute("insert into %s values (%s)" % ( - table_name, ", ".join(args) - )) - - def select(self, table_name: str, **params) -> list: - self.execute("select * from %s where %s" % ( - table_name, " and ".join( - [f"{key}={params[key]}" for key in params]) - )) - return self.fetchall() - - def exist(self, table_name: str, **params) -> bool: - return bool(self.select(table_name, **params)) - - def update(self, table_name: str, *args, **params): - args = " ".join(args) - params = " ".join([f"{key}={params[key]}" for key in params]) - self.execute("update %s set %s where %s" % ( - table_name, args, params - )) - self.commit() - - def delete(self, table_name: str, **params): - self.execute("select * from %s where %s" % ( - table_name, " ".join([f"{key}={params[key]}" for key in params]) - )) - self.commit() - - def count(self, table_name: str) -> int: - try: - self.execute("select count(*) from %s" % (table_name)) - return self.fetchone()[0] - except sqlite3.OperationalError: - return -1 \ No newline at end of file