Skip to content

Commit b9ba1cd

Browse files
authored
feat(client): refactor PushDeliver to support DI & adapt image parsing to the new protocol (#16)
* feat(utils): change the request method of sign to POST * refactor(client): refactor `PushDeliver` to support dependency injection of Client * fix(client): fixed parsing images in the new protocol - ref: LagrangeDev/Lagrange.Core@ef6b41b
1 parent f7d1ff7 commit b9ba1cd

File tree

9 files changed

+146
-44
lines changed

9 files changed

+146
-44
lines changed

lagrange/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import asyncio
33

44
from .client.client import Client as Client
5+
from .client.server_push.msg import msg_push_handler
6+
from .client.server_push.service import server_kick_handler
57
from .utils.log import log as log
68
from .utils.log import install_loguru as install_loguru
79
from .utils.sign import sign_provider
@@ -49,6 +51,8 @@ async def run(self):
4951
)
5052
for event, handler in self.events.items():
5153
self.client.events.subscribe(event, handler)
54+
self.client.push_deliver.subscribe("trpc.msg.olpush.OlPushService.MsgPush", msg_push_handler)
55+
self.client.push_deliver.subscribe("trpc.qq_new_tech.status_svc.StatusService.KickNT", server_kick_handler)
5256
self.client.connect()
5357
status = await self.login(self.client)
5458
if not status:

lagrange/client/client.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import os
22
import struct
3+
import asyncio
34
from io import BytesIO
4-
from typing import BinaryIO, Callable, Coroutine, List, Optional, Union, overload
5+
from typing import BinaryIO, Callable, Coroutine, List, Optional, Union, overload, Literal
56

67
from lagrange.info import AppInfo, DeviceInfo, SigInfo
78
from lagrange.pb.message.msg_push import MsgPushBody
@@ -32,6 +33,7 @@
3233
PBGetInfoFromUidReq,
3334
)
3435
from lagrange.pb.service.oidb import OidbRequest, OidbResponse
36+
from lagrange.pb.highway.comm import IndexNode
3537
from lagrange.utils.binary.protobuf import proto_decode, proto_encode
3638
from lagrange.utils.log import log
3739
from lagrange.utils.operator import timestamp
@@ -48,7 +50,7 @@
4850
from .message.encoder import build_message
4951
from .message.types import Element
5052
from .models import UserInfo
51-
from .server_push import push_handler
53+
from .server_push.binder import PushDeliver
5254
from .wtlogin.sso import SSOPacket
5355

5456

@@ -65,12 +67,17 @@ def __init__(
6567
super().__init__(uin, app_info, device_info, sig_info, sign_provider, use_ipv6)
6668

6769
self._events = Events()
70+
self._push_deliver = PushDeliver(self)
6871
self._highway = HighWaySession(self)
6972

7073
@property
7174
def events(self) -> Events:
7275
return self._events
7376

77+
@property
78+
def push_deliver(self) -> PushDeliver:
79+
return self._push_deliver
80+
7481
async def register(self) -> bool:
7582
if await super().register():
7683
self._events.emit(ClientOnline(), self)
@@ -155,8 +162,7 @@ async def send_oidb_svc(
155162
return rsp
156163

157164
async def push_handler(self, sso: SSOPacket):
158-
rsp = await push_handler.execute(sso.cmd, sso)
159-
if rsp:
165+
if rsp := await self._push_deliver.execute(sso.cmd, sso):
160166
self._events.emit(rsp, self)
161167

162168
async def _send_msg_raw(self, pb: dict, *, grp_id=0, uid="") -> SendMsgRsp:
@@ -229,6 +235,20 @@ async def down_grp_audio(self, audio: Audio, grp_id: int) -> BytesIO:
229235
async def down_friend_audio(self, audio: Audio) -> BytesIO:
230236
return await self._highway.download_audio(audio, uid=self.uid)
231237

238+
async def fetch_image_url(self, bus_type: Literal[10, 20], node: "IndexNode", uid=None, gid=None):
239+
if bus_type == 10:
240+
return await self._get_pri_img_url(uid, node)
241+
elif bus_type == 20:
242+
return await self._get_grp_img_url(gid, node)
243+
else:
244+
raise ValueError("bus_type must be 10 or 20")
245+
246+
async def _get_grp_img_url(self, grp_id: int, node: "IndexNode") -> str:
247+
return await self._highway.get_grp_img_url(grp_id=grp_id, node=node)
248+
249+
async def _get_pri_img_url(self, uid: str, node: "IndexNode") -> str:
250+
return await self._highway.get_pri_img_url(uid=uid, node=node)
251+
232252
async def get_grp_list(self) -> GetGrpListResponse:
233253
rsp = await self.send_oidb_svc(0xFE5, 2, PBGetGrpListRequest.build().encode())
234254
if rsp.ret_code:
@@ -292,7 +312,9 @@ async def get_grp_msg(
292312
and payload.end_seq == end
293313
), "return args not matched"
294314

295-
rsp = [parse_grp_msg(MsgPushBody.decode(i)) for i in payload.elems]
315+
rsp = list(
316+
await asyncio.gather(*[parse_grp_msg(self, MsgPushBody.decode(i)) for i in payload.elems])
317+
)
296318
if filter_deleted_msg:
297319
return [*filter(lambda msg: msg.rand != -1, rsp)]
298320
return rsp

lagrange/client/highway/encoders.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,35 @@ def encode_audio_down_req(uuid: str, grp_id: int, uid: str):
225225
)
226226

227227

228+
def encode_grp_img_download_req(grp_id: int, node: IndexNode) -> NTV2RichMediaReq:
229+
return NTV2RichMediaReq(
230+
req_head=MultiMediaReqHead(
231+
common=CommonHead(cmd=200),
232+
scene=SceneInfo(
233+
req_type=2,
234+
bus_type=1,
235+
scene_type=2,
236+
grp=GroupInfo(grp_id=grp_id),
237+
)
238+
),
239+
download=DownloadReq(node=node),
240+
)
241+
242+
243+
def encode_pri_img_download_req(uid: str, node: IndexNode) -> NTV2RichMediaReq:
244+
return NTV2RichMediaReq(
245+
req_head=MultiMediaReqHead(
246+
common=CommonHead(cmd=200),
247+
scene=SceneInfo(
248+
req_type=2,
249+
bus_type=1,
250+
scene_type=1,
251+
c2c=C2CUserInfo(uid=uid),
252+
)
253+
),
254+
download=DownloadReq(node=node),
255+
)
256+
228257
# def encode_video_upload_req(
229258
# seq: int,
230259
# from_uin: int,

lagrange/client/highway/highway.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import TYPE_CHECKING, BinaryIO, List, Optional, Tuple
77

88
from lagrange.client.message.elems import Audio, Image
9+
from lagrange.pb.highway.comm import IndexNode
910
from lagrange.pb.highway.ext import NTV2RichMediaHighwayExt
1011
from lagrange.pb.highway.httpconn import HttpConn0x6ffReq, HttpConn0x6ffRsp
1112
from lagrange.pb.highway.rsp import NTV2RichMediaResp, DownloadInfo, DownloadRsp
@@ -21,6 +22,8 @@
2122
encode_highway_head,
2223
encode_upload_img_req,
2324
encode_audio_down_req,
25+
encode_grp_img_download_req,
26+
encode_pri_img_download_req,
2427
)
2528
from .frame import read_frame, write_frame
2629
from .utils import calc_file_hash_and_length, timeit
@@ -240,6 +243,38 @@ async def upload_image(self, file: BinaryIO, gid=0, uid="") -> Image:
240243
qmsg=None if gid else ret.upload.compat_qmsg,
241244
)
242245

246+
async def get_grp_img_url(self, grp_id: int, node: "IndexNode") -> str:
247+
ret = NTV2RichMediaResp.decode(
248+
(
249+
await self._client.send_oidb_svc(
250+
0x11C4,
251+
200,
252+
encode_grp_img_download_req(
253+
grp_id, node
254+
).encode(),
255+
True
256+
)
257+
).data
258+
)
259+
body = ret.download
260+
return f"https://{body.info.domain}{body.info.url_path}{body.rkey}"
261+
262+
async def get_pri_img_url(self, uid: str, node: IndexNode) -> str:
263+
ret = NTV2RichMediaResp.decode(
264+
(
265+
await self._client.send_oidb_svc(
266+
0x11C5,
267+
200,
268+
encode_pri_img_download_req(
269+
uid, node
270+
).encode(),
271+
True
272+
)
273+
).data
274+
)
275+
body = ret.download
276+
return f"https://{body.info.domain}{body.info.url_path}{body.rkey}"
277+
243278
async def upload_voice(self, file: BinaryIO, gid=0, uid="") -> Audio:
244279
if not self._session_addr_list:
245280
await self._get_bdh_session()

lagrange/client/message/decoder.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import zlib
2-
from typing import List, Tuple, Sequence
2+
from typing import List, Tuple, Sequence, TYPE_CHECKING, cast, Literal
33

44
from lagrange.client.events.group import GroupMessage
55
from lagrange.client.events.friend import FriendMessage
@@ -9,6 +9,10 @@
99
from . import elems
1010
from .types import Element
1111
from lagrange.utils.binary.protobuf import proto_encode
12+
from lagrange.pb.highway.comm import MsgInfo
13+
14+
if TYPE_CHECKING:
15+
from lagrange.client.client import Client
1216

1317

1418
def parse_msg_info(pb: MsgPushBody) -> Tuple[int, str, int, int, int]:
@@ -31,7 +35,8 @@ def parse_friend_info(pkg: MsgPushBody) -> Tuple[int, str, int, str]:
3135
return from_uin, from_uid, to_uin, to_uid
3236

3337

34-
def parse_msg_new(rich: RichText) -> Sequence[Element]:
38+
async def parse_msg_new(client: "Client", pkg: MsgPushBody) -> Sequence[Element]:
39+
rich: RichText = pkg.message.body
3540
if rich.ptt:
3641
ptt = rich.ptt
3742
return [
@@ -134,6 +139,27 @@ def parse_msg_new(rich: RichText) -> Sequence[Element]:
134139
f8=common.pb_elem[8],
135140
)
136141
)
142+
if common.bus_type in [10, 20]: # 10: friend, 20: group
143+
extra = MsgInfo.decode(proto_encode(raw.common_elem.pb_elem))
144+
index = extra.body[0].index
145+
uid = client.uid
146+
gid = pkg.response_head.rsp_grp.gid if common.bus_type == 20 else None
147+
url = await client.fetch_image_url(bus_type=cast(Literal[10, 20], common.bus_type),
148+
node=index, uid=uid, gid=gid)
149+
msg_chain.append(
150+
elems.Image(
151+
name=index.info.name,
152+
size=index.info.size,
153+
id=0,
154+
md5=bytes.fromhex(index.info.hash),
155+
text=extra.biz_info.pic.summary if extra.biz_info.pic.summary else "[图片]",
156+
width=index.info.width,
157+
height=index.info.height,
158+
url=url,
159+
is_emoji=extra.biz_info.pic.biz_type != 0,
160+
qmsg=None,
161+
)
162+
)
137163
elif raw.rich_msg:
138164
service = raw.rich_msg
139165
if service.template:
@@ -227,13 +253,13 @@ def parse_msg_new(rich: RichText) -> Sequence[Element]:
227253
return msg_chain
228254

229255

230-
def parse_friend_msg(pkg: MsgPushBody) -> FriendMessage:
256+
async def parse_friend_msg(client: "Client", pkg: MsgPushBody) -> FriendMessage:
231257
from_uin, from_uid, to_uin, to_uid = parse_friend_info(pkg)
232258

233259
seq = pkg.content_head.seq
234260
msg_id = pkg.content_head.msg_id
235261
timestamp = pkg.content_head.timestamp
236-
parsed_msg = parse_msg_new(pkg.message.body)
262+
parsed_msg = await parse_msg_new(client, pkg)
237263
msg_text = "".join([getattr(msg, "text", "") for msg in parsed_msg])
238264

239265
return FriendMessage(
@@ -249,7 +275,7 @@ def parse_friend_msg(pkg: MsgPushBody) -> FriendMessage:
249275
)
250276

251277

252-
def parse_grp_msg(pkg: MsgPushBody) -> GroupMessage:
278+
async def parse_grp_msg(client: "Client", pkg: MsgPushBody) -> GroupMessage:
253279
user_id, uid, seq, time, rand = parse_msg_info(pkg)
254280

255281
grp_id = pkg.response_head.rsp_grp.gid
@@ -261,7 +287,7 @@ def parse_grp_msg(pkg: MsgPushBody) -> GroupMessage:
261287
if isinstance(grp_name, bytes): # unexpected end of data
262288
grp_name = grp_name.decode("utf-8", errors="ignore")
263289

264-
parsed_msg = parse_msg_new(pkg.message.body)
290+
parsed_msg = await parse_msg_new(client, pkg)
265291
msg_text = "".join([getattr(msg, "text", "") for msg in parsed_msg])
266292

267293
return GroupMessage(
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +0,0 @@
1-
from . import msg, service
2-
from .binder import push_handler
3-
4-
__all__ = ["push_handler"]
Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,25 @@
1-
import functools
2-
from typing import Any, Callable, Coroutine, Dict
1+
from typing import Any, Callable, Coroutine, Dict, TYPE_CHECKING
32

43
from lagrange.client.wtlogin.sso import SSOPacket
54

65
from .log import logger
76

7+
if TYPE_CHECKING:
8+
from lagrange.client.client import Client
9+
810

911
class PushDeliver:
10-
def __init__(self):
12+
def __init__(self, client: "Client"):
13+
self._client = client
1114
self._handle_map: Dict[
12-
str, Callable[[SSOPacket], Coroutine[None, None, Any]]
15+
str, Callable[["Client", SSOPacket], Coroutine[None, None, Any]]
1316
] = {}
1417

15-
def subscribe(self, cmd: str):
16-
def _decorator(
17-
func: Callable[[SSOPacket], Coroutine[None, None, Any]]
18-
) -> Callable[[SSOPacket], Coroutine[None, None, Any]]:
19-
@functools.wraps(func)
20-
async def _wrapper(packet: SSOPacket):
21-
return await func(packet)
22-
23-
self._handle_map[cmd] = _wrapper # noqa
24-
return _wrapper
25-
26-
return _decorator
18+
def subscribe(self, cmd: str, func: Callable[["Client", SSOPacket], Coroutine[None, None, Any]]):
19+
self._handle_map[cmd] = func
2720

2821
async def execute(self, cmd: str, sso: SSOPacket):
2922
if cmd not in self._handle_map:
30-
logger.warning("unsupported command: {}".format(cmd))
23+
logger.warning(f"Unsupported command: {cmd}")
3124
else:
32-
return await self._handle_map[cmd](sso)
33-
34-
35-
push_handler = PushDeliver()
25+
return await self._handle_map[cmd](self._client, sso)

lagrange/client/server_push/msg.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import re
3+
from typing import TYPE_CHECKING
34

45
from lagrange.client.message.decoder import parse_grp_msg, parse_friend_msg
56
from lagrange.pb.message.msg_push import MsgPush
@@ -26,21 +27,22 @@
2627
GroupRecall,
2728
)
2829
from ..wtlogin.sso import SSOPacket
29-
from .binder import push_handler
3030
from .log import logger
3131

32+
if TYPE_CHECKING:
33+
from lagrange.client.client import Client
3234

33-
@push_handler.subscribe("trpc.msg.olpush.OlPushService.MsgPush")
34-
async def msg_push_handler(sso: SSOPacket):
35+
36+
async def msg_push_handler(client: "Client", sso: SSOPacket):
3537
pkg = MsgPush.decode(sso.data).body
3638
typ = pkg.content_head.type
3739
sub_typ = pkg.content_head.sub_type
3840

3941
logger.debug("msg_push received, type: {}.{}".format(typ, sub_typ))
4042
if typ == 82: # grp msg
41-
return parse_grp_msg(pkg)
43+
return await parse_grp_msg(client, pkg)
4244
elif typ == 166: # frd msg
43-
return parse_friend_msg(pkg)
45+
return await parse_friend_msg(client, pkg)
4446
elif typ == 33: # member joined
4547
pb = MemberChanged.decode(pkg.message.buf2)
4648
return GroupMemberJoined(

lagrange/client/server_push/service.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33
from ..events.service import ServerKick
44
from ..wtlogin.sso import SSOPacket
5-
from .binder import push_handler
65

76

8-
@push_handler.subscribe("trpc.qq_new_tech.status_svc.StatusService.KickNT")
9-
async def server_kick(sso: SSOPacket):
7+
async def server_kick_handler(_, sso: SSOPacket):
108
ev = KickNT.decode(sso.data)
119
return ServerKick(tips=ev.tips, title=ev.title)

0 commit comments

Comments
 (0)