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 = """- {title}{content}
"""
-
-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