From f05079e46a1f836c5cdd370b1674b50ddcd542de Mon Sep 17 00:00:00 2001 From: Decrabbit Date: Mon, 11 Nov 2024 21:31:32 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(message):=20=E5=88=9D=E6=AD=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=BD=AC=E5=8F=91=E6=B6=88=E6=81=AF=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 MulitMsg 类型,用于表示多条消息的转发 - 实现 build_forward_msg 函数,用于构建转发消息的 LongMsgResult - 添加 _get_mulitmsg_resid 函数,用于获取转发消息的 resid - 在 message/types.py 中添加 MulitMsg 类型的引入 - 在 message/decoder.py 中更新 parse_friend_msg 函数,使用 msg_id 替代 random --- lagrange/client/client.py | 4 +- lagrange/client/message/decoder.py | 2 +- lagrange/client/message/elems.py | 49 +++++++++- lagrange/client/message/encoder.py | 141 +++++++++++++++++++++-------- lagrange/client/message/types.py | 2 + lagrange/pb/message/heads.py | 19 +++- lagrange/pb/message/longmsg.py | 66 ++++++++++++++ 7 files changed, 240 insertions(+), 43 deletions(-) create mode 100644 lagrange/pb/message/longmsg.py diff --git a/lagrange/client/client.py b/lagrange/client/client.py index e978289..ebe1da5 100644 --- a/lagrange/client/client.py +++ b/lagrange/client/client.py @@ -210,13 +210,13 @@ async def _send_msg_raw(self, pb: dict, *, grp_id=0, uid="") -> SendMsgRsp: return SendMsgRsp.decode(packet.data) async def send_grp_msg(self, msg_chain: list[Element], grp_id: int) -> int: - result = await self._send_msg_raw({1: build_message(msg_chain).encode()}, grp_id=grp_id) + result = await self._send_msg_raw({1: (await build_message(msg_chain)).encode()}, grp_id=grp_id) if result.ret_code: raise AssertionError(result.ret_code, result.err_msg) return result.seq async def send_friend_msg(self, msg_chain: list[Element], uid: str) -> int: - result = await self._send_msg_raw({1: build_message(msg_chain).encode()}, uid=uid) + result = await self._send_msg_raw({1: (await build_message(msg_chain)).encode()}, uid=uid) if result.ret_code: raise AssertionError(result.ret_code, result.err_msg) return result.seq diff --git a/lagrange/client/message/decoder.py b/lagrange/client/message/decoder.py index 0a47cc6..4f5337d 100644 --- a/lagrange/client/message/decoder.py +++ b/lagrange/client/message/decoder.py @@ -299,7 +299,7 @@ async def parse_friend_msg(client: "Client", pkg: MsgPushBody) -> FriendMessage: from_uin, from_uid, to_uin, to_uid = parse_friend_info(pkg) seq = pkg.content_head.seq - msg_id = pkg.content_head.msg_id + msg_id = pkg.content_head.random timestamp = pkg.content_head.timestamp parsed_msg = await parse_msg_new(client, pkg, fri_id=from_uid, grp_id=None) msg_text = "".join([getattr(msg, "display", "") for msg in parsed_msg]) diff --git a/lagrange/client/message/elems.py b/lagrange/client/message/elems.py index 093bee3..4bad04e 100644 --- a/lagrange/client/message/elems.py +++ b/lagrange/client/message/elems.py @@ -1,10 +1,14 @@ import json from dataclasses import dataclass, field -from typing import Optional +import time +from typing import TYPE_CHECKING, Optional from lagrange.client.events.group import GroupMessage from lagrange.info.serialize import JsonSerializer +if TYPE_CHECKING: + from .types import Element + @dataclass class BaseElem(JsonSerializer): @@ -15,6 +19,10 @@ def display(self) -> str: @property def type(self) -> str: return self.__class__.__name__.lower() + + @property + def raw_text(self) -> str: + return "" @dataclass @@ -47,6 +55,10 @@ class Text(BaseElem): @property def display(self) -> str: return self.text + + @property + def raw_text(self) -> str: + return self.text @dataclass @@ -111,6 +123,14 @@ class At(BaseElem): @classmethod def build(cls, ev: GroupMessage) -> "At": return cls(uin=ev.uin, uid=ev.uid, text=f"@{ev.nickname or ev.uin}") + + @property + def display(self) -> str: + return self.text + + @property + def raw_text(self) -> str: + return self.text @dataclass @@ -216,6 +236,9 @@ class File(CompatibleText): def display(self) -> str: return f"[file:{self.file_name}]" + @property + def raw_text(self) -> str: + return "[文件]" @classmethod def _paste_build( cls, @@ -267,6 +290,10 @@ class Markdown(BaseElem): @property def display(self) -> str: return f"[markdown:{self.content}]" + + @property + def raw_text(self) -> str: + return "[Markdown]" class Permission: @@ -315,3 +342,23 @@ class Keyboard(BaseElem): @property def display(self) -> str: return f"[keyboard:{self.bot_appid}]" + + +@dataclass +class ForwardNode(BaseElem): + sender_uin: int + sender_nick: str + sender_avatar: str + + content: list["Element"] + timestamp: int = field(default_factory=lambda: int(time.time())) + + +@dataclass +class MulitMsg(BaseElem): + messages: list[ForwardNode] + resid: Optional[str] = None + + @property + def display(self) -> str: + return f"[forward:{self.resid}]" diff --git a/lagrange/client/message/encoder.py b/lagrange/client/message/encoder.py index 72b7dee..2e28f6f 100644 --- a/lagrange/client/message/encoder.py +++ b/lagrange/client/message/encoder.py @@ -1,8 +1,17 @@ +import gzip import json +import random import struct +from uuid import uuid4 import zlib -from typing import Optional +from typing import Any, Callable, Optional +from collections.abc import Coroutine +from lagrange.client.client import Client +from lagrange.pb.message.heads import ContentHead, Forward, ResponseHead +from lagrange.pb.message.longmsg import LongMsgAction, LongMsgActionBody, LongMsgRespResult, LongMsgResult, LongMsgRsp +from lagrange.pb.message.msg import Message +from lagrange.pb.message.msg_push import MsgPushBody from lagrange.pb.message.rich_text import Elems, RichText from lagrange.pb.message.rich_text.elems import ( CustomFace, @@ -29,6 +38,7 @@ Emoji, Image, Json, + MulitMsg, Quote, Raw, Reaction, @@ -41,7 +51,9 @@ from .types import Element -def build_message(msg_chain: list[Element], compatible=True) -> RichText: +async def build_message( + msg_chain: list[Element], compatible=True, forward_func: Optional[Callable[..., Coroutine[Any, Any, str]]] = None +) -> RichText: if not msg_chain: raise ValueError("Message chain is empty") msg_pb: list[Elems] = [] @@ -62,9 +74,7 @@ def build_message(msg_chain: list[Element], compatible=True) -> RichText: Elems( text=PBText( string=msg.text, - attr6_buf=struct.pack( - "!xb3xbbI2x", 1, len(msg.text), 0, msg.uin - ), + attr6_buf=struct.pack("!xb3xbbI2x", 1, len(msg.text), 0, msg.uin), pb_reserved={3: 2, 4: 0, 5: 0, 9: msg.uid, 11: 0}, ) ) @@ -87,9 +97,7 @@ def build_message(msg_chain: list[Element], compatible=True) -> RichText: Elems( text=PBText( string=text, - attr6_buf=struct.pack( - "!xb3xbbI2x", 1, len(text), 0, msg.uin - ), + attr6_buf=struct.pack("!xb3xbbI2x", 1, len(text), 0, msg.uin), pb_reserved={3: 2, 4: 0, 5: 0, 9: msg.uid, 11: 0}, ) ) @@ -97,9 +105,7 @@ def build_message(msg_chain: list[Element], compatible=True) -> RichText: elif isinstance(msg, Emoji): msg_pb.append(Elems(face=Face(index=msg.id))) elif isinstance(msg, Json): - msg_pb.append( - Elems(mini_app=MiniApp(template=b"\x01" + zlib.compress(msg.raw))) - ) + msg_pb.append(Elems(mini_app=MiniApp(template=b"\x01" + zlib.compress(msg.raw)))) elif isinstance(msg, Image): if msg.id: # customface msg_pb.append( @@ -116,24 +122,15 @@ def build_message(msg_chain: list[Element], compatible=True) -> RichText: size=msg.size, args=ImageReserveArgs( is_emoji=msg.is_emoji, - display_name=msg.display_name - or ("[动画表情]" if msg.is_emoji else "[图片]"), + display_name=msg.display_name or ("[动画表情]" if msg.is_emoji else "[图片]"), ), ) ) ) else: - msg_pb.append( - Elems(not_online_image=NotOnlineImage.decode(msg.qmsg)) - ) + msg_pb.append(Elems(not_online_image=NotOnlineImage.decode(msg.qmsg))) elif isinstance(msg, Service): - msg_pb.append( - Elems( - rich_msg=RichMsg( - template=b"\x01" + zlib.compress(msg.raw), service_id=msg.id - ) - ) - ) + msg_pb.append(Elems(rich_msg=RichMsg(template=b"\x01" + zlib.compress(msg.raw), service_id=msg.id))) elif isinstance(msg, Raw): msg_pb.append(Elems(open_data=OpenData(data=msg.data))) elif isinstance(msg, Reaction): @@ -180,19 +177,15 @@ def build_message(msg_chain: list[Element], compatible=True) -> RichText: ) ) elif isinstance(msg, GreyTips): - content = json.dumps({ - "gray_tip": msg.text, - "object_type": 3, - "sub_type": 2, - "type": 4, - }) - msg_pb.append( - Elems( - general_flags=GeneralFlags( - PbReserve=PBGreyTips.build(content) - ) - ) + content = json.dumps( + { + "gray_tip": msg.text, + "object_type": 3, + "sub_type": 2, + "type": 4, + } ) + msg_pb.append(Elems(general_flags=GeneralFlags(PbReserve=PBGreyTips.build(content)))) elif isinstance(msg, Text): msg_pb.append(Elems(text=PBText(string=msg.text))) elif isinstance(msg, Poke): @@ -205,7 +198,34 @@ def build_message(msg_chain: list[Element], compatible=True) -> RichText: ) ) ) - + elif isinstance(msg, MulitMsg): + if msg.resid is None: + if forward_func is None: + continue + msg.resid = await forward_func(msg) + fileid = uuid4() + template = { + "app": "com.tencent.multimsg", + "config": {"autosize": 1, "forward": 1, "round": 1, "type": "normal", "width": 300}, + "desc": "[聊天记录]", + "extra": f'{json.dumps({"filename":fileid,"tsum":len(msg.messages)})}\n', + "meta": { + "detail": { + "news": [ + {"text": "".join(element.raw_text for element in forward_node.content)} + for forward_node in msg.messages + ], + "resid": msg.resid, + "source": "群聊的聊天记录", + "summary": f"查看{len(msg.messages)}条转发消息", + "uniseq": f"{fileid}", + } + }, + "prompt": "[聊天记录]", + "ver": "0.0.0.5", + "view": "contact", + } + msg_pb.append(Elems(mini_app=MiniApp(template=b"\x01" + zlib.compress(json.dumps(template).encode())))) else: raise NotImplementedError else: @@ -222,3 +242,52 @@ def build_message(msg_chain: list[Element], compatible=True) -> RichText: else: # friend msg_ptt = Ptt.decode(audio.qmsg) return RichText(content=msg_pb, ptt=msg_ptt) + + +async def build_forward_msg( + forword_msg: MulitMsg, forward_func: Callable[..., Coroutine[Any, Any, str]] +) -> LongMsgResult: + start_seq = random.randint(1000000, 9999999) + return LongMsgResult( + action=LongMsgAction( + action_command="MultiMsg", + action_data=LongMsgActionBody( + action_list=[ + MsgPushBody( + response_head=ResponseHead(from_uin=node.sender_uin), + content_head=ContentHead( + type=82, + random=random.randint(100000000, 2147483647), + seq=seq, + timestamp=node.timestamp, + forward=Forward(), + ), + message=Message(body=await build_message(node.content, forward_func=forward_func)), + ) + for seq, node in enumerate(forword_msg.messages, start_seq) + ] + ), + ) + ) + + +#it should be enterpoint +async def _get_mulitmsg_resid( + client: Client, forword_msg: MulitMsg, target: str = "", grp_id: Optional[int] = None +) -> str: + body = await build_forward_msg(forword_msg, get_resid_func(client, target, grp_id)) + packet = await client.send_uni_packet( + "trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", + LongMsgRsp.build(gzip.compress(body.encode()), target, grp_id).encode(), + ) + result = LongMsgRespResult.decode(packet.data) + return result.resid + + +def get_resid_func( + client: Client, target: str = "", grp_id: Optional[int] = None +) -> Callable[..., Coroutine[Any, Any, str]]: + async def wrap(forword_msg: MulitMsg): + return await _get_mulitmsg_resid(client, forword_msg, target, grp_id) + + return wrap diff --git a/lagrange/client/message/types.py b/lagrange/client/message/types.py index 4624659..ed94244 100644 --- a/lagrange/client/message/types.py +++ b/lagrange/client/message/types.py @@ -20,6 +20,7 @@ File, Markdown, Keyboard, + MulitMsg, ) # T = TypeVar( @@ -54,4 +55,5 @@ "File", "Markdown", "Keyboard", + "MulitMsg", ] diff --git a/lagrange/pb/message/heads.py b/lagrange/pb/message/heads.py index 1af7522..c6231b8 100644 --- a/lagrange/pb/message/heads.py +++ b/lagrange/pb/message/heads.py @@ -5,12 +5,17 @@ class ContentHead(ProtoStruct): type: int = proto_field(1) - sub_type: int = proto_field(2, default=0) - msg_id: int = proto_field(4, default=0) + sub_type: Optional[int] = proto_field(2, default=None) # when send ,private is 175, group is None + f3: Optional[int] = proto_field(3, default=None) # In forward msg, this field like sub_type + random: int = proto_field(4, default=0) seq: int = proto_field(5, default=0) timestamp: int = proto_field(6, default=0) - rand: int = proto_field(7, default=0) + pkg_num: int = proto_field(7, default=1) + pkg_index: int = proto_field(8, default=0) + div_seq: int = proto_field(9, default=0) + c2c_seq: Optional[int] = proto_field(11, default=None) # new_id: int = proto_field(12) + forward: Optional["Forward"] = proto_field(15, default=None) class Grp(ProtoStruct): @@ -27,3 +32,11 @@ class ResponseHead(ProtoStruct): to_uin: int = proto_field(5, default=0) to_uid: str = proto_field(6, default="") rsp_grp: Optional[Grp] = proto_field(8, default=None) + + +class Forward(ProtoStruct): + f1: int = proto_field(1, default=0) + f2: int = proto_field(2, default=0) + f3: int = proto_field(3, default=0) + f4: bytes = proto_field(4, default=b"") + f5: bytes = proto_field(5, default=b"") diff --git a/lagrange/pb/message/longmsg.py b/lagrange/pb/message/longmsg.py new file mode 100644 index 0000000..d5ccdcf --- /dev/null +++ b/lagrange/pb/message/longmsg.py @@ -0,0 +1,66 @@ +from typing import Optional +from lagrange.pb.message.msg_push import MsgPushBody +from lagrange.utils.binary.protobuf.models import ProtoStruct, proto_field + + +class LongMsgCfg(ProtoStruct): + f1: int = proto_field(1) + f2: int = proto_field(2) + f3: int = proto_field(3) + f4: Optional[int] = proto_field(4, default=None) + + +class LongMsgRespResult(ProtoStruct): + resid: str = proto_field(3) + + +class LongMsgResp(ProtoStruct): + result: LongMsgRespResult = proto_field(2) + cfg: LongMsgCfg = proto_field(15) + + +class LongMsgRsp(ProtoStruct): + msg_info: "LongMsgBody" = proto_field(2) + cfg: LongMsgCfg = proto_field(15) + + @classmethod + def build(cls, msg_content: bytes, target: str = "", grp_id: Optional[int] = None): + cfg = LongMsgCfg(f1=4, f2=1, f3=7, f4=0) + if grp_id: + return cls(msg_info=LongMsgBody.build_group(grp_id, msg_content), cfg=cfg) + elif target: + return cls(msg_info=LongMsgBody.build_friend(target, msg_content), cfg=cfg) + else: + raise ValueError("Must have target or grp_id") + + +class LongMsgBody(ProtoStruct): + f1: int = proto_field(1) # grp 3,friend 1 + gid_or_uid: "MulitMsgProperty" = proto_field(2) + grp_id: Optional[int] = proto_field(3, default=None) + msg_content: bytes = proto_field(4) + + @classmethod + def build_friend(cls, target: str, msg_content: bytes): + return cls(f1=1, gid_or_uid=MulitMsgProperty(value=target), msg_content=msg_content) + + @classmethod + def build_group(cls, grp_id: int, msg_content: bytes): + return cls(f1=3, gid_or_uid=MulitMsgProperty(value=str(grp_id)), grp_id=grp_id, msg_content=msg_content) + + +class MulitMsgProperty(ProtoStruct): + value: str = proto_field(2) # uid or grp_id + + +class LongMsgResult(ProtoStruct): + action: "LongMsgAction" = proto_field(2) + + +class LongMsgAction(ProtoStruct): + action_command: str = proto_field(1) + action_data: "LongMsgActionBody" = proto_field(2) + + +class LongMsgActionBody(ProtoStruct): + action_list: list[MsgPushBody] = proto_field(1) From 059c83ee6f725a21536e25c4ac101baa39b3a94d Mon Sep 17 00:00:00 2001 From: Decrabbit Date: Mon, 11 Nov 2024 21:37:56 +0800 Subject: [PATCH 2/4] fix typing error --- lagrange/client/message/encoder.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lagrange/client/message/encoder.py b/lagrange/client/message/encoder.py index 2e28f6f..7d05660 100644 --- a/lagrange/client/message/encoder.py +++ b/lagrange/client/message/encoder.py @@ -4,10 +4,9 @@ import struct from uuid import uuid4 import zlib -from typing import Any, Callable, Optional +from typing import TYPE_CHECKING, Any, Callable, Optional from collections.abc import Coroutine -from lagrange.client.client import Client from lagrange.pb.message.heads import ContentHead, Forward, ResponseHead from lagrange.pb.message.longmsg import LongMsgAction, LongMsgActionBody, LongMsgRespResult, LongMsgResult, LongMsgRsp from lagrange.pb.message.msg import Message @@ -50,6 +49,9 @@ ) from .types import Element +if TYPE_CHECKING: + from ..client import Client as Client + async def build_message( msg_chain: list[Element], compatible=True, forward_func: Optional[Callable[..., Coroutine[Any, Any, str]]] = None @@ -271,9 +273,9 @@ async def build_forward_msg( ) -#it should be enterpoint +# it should be enterpoint async def _get_mulitmsg_resid( - client: Client, forword_msg: MulitMsg, target: str = "", grp_id: Optional[int] = None + client: "Client", forword_msg: MulitMsg, target: str = "", grp_id: Optional[int] = None ) -> str: body = await build_forward_msg(forword_msg, get_resid_func(client, target, grp_id)) packet = await client.send_uni_packet( @@ -285,7 +287,7 @@ async def _get_mulitmsg_resid( def get_resid_func( - client: Client, target: str = "", grp_id: Optional[int] = None + client: "Client", target: str = "", grp_id: Optional[int] = None ) -> Callable[..., Coroutine[Any, Any, str]]: async def wrap(forword_msg: MulitMsg): return await _get_mulitmsg_resid(client, forword_msg, target, grp_id) From 7415c1c903f15d6bcb5e02396c7738a7034fac1e Mon Sep 17 00:00:00 2001 From: Decrabbit Date: Mon, 11 Nov 2024 23:43:23 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(client):=20=E6=94=AF=E6=8C=81=E5=8F=91?= =?UTF-8?q?=E9=80=81=E5=90=88=E5=B9=B6=E6=B6=88=E6=81=AF=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增发送群聊和好友合并消息的功能 - 优化合并消息的构建和解析逻辑 - 修复了一些与合并消息相关的小问题 --- lagrange/client/client.py | 28 +++++++++++++------- lagrange/client/message/elems.py | 18 +++++++------ lagrange/client/message/encoder.py | 30 +++++++++++++++------ lagrange/client/server_push/msg.py | 2 ++ lagrange/pb/message/heads.py | 33 +++++++++++------------ lagrange/pb/message/longmsg.py | 42 +++++++++++++++--------------- lagrange/pb/message/msg.py | 4 +-- 7 files changed, 93 insertions(+), 64 deletions(-) diff --git a/lagrange/client/client.py b/lagrange/client/client.py index ebe1da5..aed19eb 100644 --- a/lagrange/client/client.py +++ b/lagrange/client/client.py @@ -71,8 +71,8 @@ from .events.service import ClientOnline, ClientOffline from .highway import HighWaySession from .message.decoder import parse_grp_msg -from .message.elems import Audio, Image -from .message.encoder import build_message +from .message.elems import Audio, Image, MulitMsg +from .message.encoder import _get_mulitmsg_resid, build_message from .message.types import Element from .models import UserInfo, BotFriend from .server_push import PushDeliver, bind_services @@ -221,6 +221,20 @@ async def send_friend_msg(self, msg_chain: list[Element], uid: str) -> int: raise AssertionError(result.ret_code, result.err_msg) return result.seq + async def send_grp_forward_msg(self, forward_msg: MulitMsg, grp_id: int): + forward_msg.resid = await _get_mulitmsg_resid(self, forward_msg, grp_id=grp_id) + result = await self._send_msg_raw({1: (await build_message([forward_msg])).encode()}, grp_id=grp_id) + if result.ret_code: + raise AssertionError(result.ret_code, result.err_msg) + return result.seq + + async def send_friend_forward_msg(self, forward_msg: MulitMsg, uid: str): + forward_msg.resid = await _get_mulitmsg_resid(self, forward_msg, target=uid) + result = await self._send_msg_raw({1: (await build_message([forward_msg])).encode()},uid=uid) + if result.ret_code: + raise AssertionError(result.ret_code, result.err_msg) + return result.seq + async def upload_grp_image(self, image: BinaryIO, grp_id: int, is_emoji=False) -> Image: img = await self._highway.upload_image(image, gid=grp_id) if is_emoji: @@ -493,17 +507,13 @@ async def set_grp_request(self, grp_id: int, grp_req_seq: int, ev_type: int, act raise AssertionError(rsp.ret_code, rsp.err_msg) @overload - async def get_user_info(self, uid_or_uin: Union[str, int], /) -> UserInfo: - ... + async def get_user_info(self, uid_or_uin: Union[str, int], /) -> UserInfo: ... @overload - async def get_user_info(self, uid_or_uin: Union[list[str], list[int]], /) -> list[UserInfo]: - ... + async def get_user_info(self, uid_or_uin: Union[list[str], list[int]], /) -> list[UserInfo]: ... async def get_user_info( - self, - uid_or_uin: Union[str, int, list[str], list[int]], - / + self, uid_or_uin: Union[str, int, list[str], list[int]], / ) -> Union[UserInfo, list[UserInfo]]: if isinstance(uid_or_uin, list): assert uid_or_uin, "empty uid or uin" diff --git a/lagrange/client/message/elems.py b/lagrange/client/message/elems.py index 4bad04e..a749f3c 100644 --- a/lagrange/client/message/elems.py +++ b/lagrange/client/message/elems.py @@ -19,7 +19,7 @@ def display(self) -> str: @property def type(self) -> str: return self.__class__.__name__.lower() - + @property def raw_text(self) -> str: return "" @@ -55,7 +55,7 @@ class Text(BaseElem): @property def display(self) -> str: return self.text - + @property def raw_text(self) -> str: return self.text @@ -123,11 +123,11 @@ class At(BaseElem): @classmethod def build(cls, ev: GroupMessage) -> "At": return cls(uin=ev.uin, uid=ev.uid, text=f"@{ev.nickname or ev.uin}") - + @property def display(self) -> str: return self.text - + @property def raw_text(self) -> str: return self.text @@ -239,6 +239,7 @@ def display(self) -> str: @property def raw_text(self) -> str: return "[文件]" + @classmethod def _paste_build( cls, @@ -290,7 +291,7 @@ class Markdown(BaseElem): @property def display(self) -> str: return f"[markdown:{self.content}]" - + @property def raw_text(self) -> str: return "[Markdown]" @@ -346,11 +347,12 @@ def display(self) -> str: @dataclass class ForwardNode(BaseElem): + content: list["Element"] + sender_uin: int - sender_nick: str - sender_avatar: str + sender_nick: str = "QQ用户" + sender_avatar_url: str = "" - content: list["Element"] timestamp: int = field(default_factory=lambda: int(time.time())) diff --git a/lagrange/client/message/encoder.py b/lagrange/client/message/encoder.py index 7d05660..9c62853 100644 --- a/lagrange/client/message/encoder.py +++ b/lagrange/client/message/encoder.py @@ -7,8 +7,14 @@ from typing import TYPE_CHECKING, Any, Callable, Optional from collections.abc import Coroutine -from lagrange.pb.message.heads import ContentHead, Forward, ResponseHead -from lagrange.pb.message.longmsg import LongMsgAction, LongMsgActionBody, LongMsgRespResult, LongMsgResult, LongMsgRsp +from lagrange.pb.message.heads import ContentHead, Forward, Grp, ResponseHead +from lagrange.pb.message.longmsg import ( + LongMsgAction, + LongMsgActionBody, + LongMsgResp, + LongMsgResult, + LongMsgRsp, +) from lagrange.pb.message.msg import Message from lagrange.pb.message.msg_push import MsgPushBody from lagrange.pb.message.rich_text import Elems, RichText @@ -205,7 +211,7 @@ async def build_message( if forward_func is None: continue msg.resid = await forward_func(msg) - fileid = uuid4() + fileid = str(uuid4()) template = { "app": "com.tencent.multimsg", "config": {"autosize": 1, "forward": 1, "round": 1, "type": "normal", "width": 300}, @@ -214,7 +220,9 @@ async def build_message( "meta": { "detail": { "news": [ - {"text": "".join(element.raw_text for element in forward_node.content)} + { + "text": f'{forward_node.sender_nick}:{"".join(element.raw_text for element in forward_node.content)}' + } for forward_node in msg.messages ], "resid": msg.resid, @@ -256,13 +264,18 @@ async def build_forward_msg( action_data=LongMsgActionBody( action_list=[ MsgPushBody( - response_head=ResponseHead(from_uin=node.sender_uin), + response_head=ResponseHead( + from_uin=node.sender_uin, rsp_grp=Grp(sender_name=node.sender_nick, f5=2) + ), content_head=ContentHead( type=82, random=random.randint(100000000, 2147483647), seq=seq, timestamp=node.timestamp, - forward=Forward(), + forward=Forward( + custom_flag=b"666" if node.sender_nick or node.sender_avatar_url else b"", + avatar_url=node.sender_avatar_url, + ), ), message=Message(body=await build_message(node.content, forward_func=forward_func)), ) @@ -278,12 +291,13 @@ async def _get_mulitmsg_resid( client: "Client", forword_msg: MulitMsg, target: str = "", grp_id: Optional[int] = None ) -> str: body = await build_forward_msg(forword_msg, get_resid_func(client, target, grp_id)) + packet = await client.send_uni_packet( "trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", LongMsgRsp.build(gzip.compress(body.encode()), target, grp_id).encode(), ) - result = LongMsgRespResult.decode(packet.data) - return result.resid + result = LongMsgResp.decode(packet.data) + return result.result.resid def get_resid_func( diff --git a/lagrange/client/server_push/msg.py b/lagrange/client/server_push/msg.py index b6114e1..68a6bcf 100644 --- a/lagrange/client/server_push/msg.py +++ b/lagrange/client/server_push/msg.py @@ -77,6 +77,8 @@ async def msg_push_handler(client: "Client", sso: SSOPacket): pkg = MsgPush.decode(sso.data).body typ = pkg.content_head.type sub_typ = pkg.content_head.sub_type + if sub_typ is None: + sub_typ=-1 logger.debug(f"msg_push received, type: {typ}.{sub_typ}") if typ == 82: # grp msg diff --git a/lagrange/pb/message/heads.py b/lagrange/pb/message/heads.py index c6231b8..06a952c 100644 --- a/lagrange/pb/message/heads.py +++ b/lagrange/pb/message/heads.py @@ -3,6 +3,14 @@ from lagrange.utils.binary.protobuf import proto_field, ProtoStruct +class Forward(ProtoStruct): + f1: int = proto_field(1, default=0) + f2: int = proto_field(2, default=0) + f3: int = proto_field(3, default=0) + custom_flag: bytes = proto_field(4, default=b"")#好弱智,不设置不显示自定义名字和头像 + avatar_url: str = proto_field(5, default=b"")#input costom url + + class ContentHead(ProtoStruct): type: int = proto_field(1) sub_type: Optional[int] = proto_field(2, default=None) # when send ,private is 175, group is None @@ -15,28 +23,21 @@ class ContentHead(ProtoStruct): div_seq: int = proto_field(9, default=0) c2c_seq: Optional[int] = proto_field(11, default=None) # new_id: int = proto_field(12) - forward: Optional["Forward"] = proto_field(15, default=None) + forward: Optional[Forward] = proto_field(15, default=None) class Grp(ProtoStruct): gid: int = proto_field(1, default=0) sender_name: str = proto_field(4, default="") # empty in get_grp_msg - grp_name: str = proto_field(7, default="") + f5: Optional[int] = proto_field(5, default=None) + grp_name: Optional[str] = proto_field(7, default=None) class ResponseHead(ProtoStruct): - from_uin: int = proto_field(1, default=0) - from_uid: str = proto_field(2, default="") - type: int = proto_field(3, default=0) - sigmap: int = proto_field(4, default=0) - to_uin: int = proto_field(5, default=0) - to_uid: str = proto_field(6, default="") + from_uin: Optional[int] = proto_field(1, default=None) + from_uid: Optional[str] = proto_field(2, default=None) + type: Optional[int] = proto_field(3, default=None) + sigmap: Optional[int] = proto_field(4, default=None) + to_uin: Optional[int] = proto_field(5, default=None) + to_uid: Optional[str] = proto_field(6, default=None) rsp_grp: Optional[Grp] = proto_field(8, default=None) - - -class Forward(ProtoStruct): - f1: int = proto_field(1, default=0) - f2: int = proto_field(2, default=0) - f3: int = proto_field(3, default=0) - f4: bytes = proto_field(4, default=b"") - f5: bytes = proto_field(5, default=b"") diff --git a/lagrange/pb/message/longmsg.py b/lagrange/pb/message/longmsg.py index d5ccdcf..6349682 100644 --- a/lagrange/pb/message/longmsg.py +++ b/lagrange/pb/message/longmsg.py @@ -19,24 +19,13 @@ class LongMsgResp(ProtoStruct): cfg: LongMsgCfg = proto_field(15) -class LongMsgRsp(ProtoStruct): - msg_info: "LongMsgBody" = proto_field(2) - cfg: LongMsgCfg = proto_field(15) - - @classmethod - def build(cls, msg_content: bytes, target: str = "", grp_id: Optional[int] = None): - cfg = LongMsgCfg(f1=4, f2=1, f3=7, f4=0) - if grp_id: - return cls(msg_info=LongMsgBody.build_group(grp_id, msg_content), cfg=cfg) - elif target: - return cls(msg_info=LongMsgBody.build_friend(target, msg_content), cfg=cfg) - else: - raise ValueError("Must have target or grp_id") +class MulitMsgProperty(ProtoStruct): + value: str = proto_field(2) # uid or grp_id class LongMsgBody(ProtoStruct): f1: int = proto_field(1) # grp 3,friend 1 - gid_or_uid: "MulitMsgProperty" = proto_field(2) + gid_or_uid: MulitMsgProperty = proto_field(2) grp_id: Optional[int] = proto_field(3, default=None) msg_content: bytes = proto_field(4) @@ -49,18 +38,29 @@ def build_group(cls, grp_id: int, msg_content: bytes): return cls(f1=3, gid_or_uid=MulitMsgProperty(value=str(grp_id)), grp_id=grp_id, msg_content=msg_content) -class MulitMsgProperty(ProtoStruct): - value: str = proto_field(2) # uid or grp_id +class LongMsgRsp(ProtoStruct): + msg_info: LongMsgBody = proto_field(2) + cfg: LongMsgCfg = proto_field(15) + @classmethod + def build(cls, msg_content: bytes, target: str = "", grp_id: Optional[int] = None): + cfg = LongMsgCfg(f1=4, f2=1, f3=7, f4=0) + if grp_id: + return cls(msg_info=LongMsgBody.build_group(grp_id, msg_content), cfg=cfg) + elif target: + return cls(msg_info=LongMsgBody.build_friend(target, msg_content), cfg=cfg) + else: + raise ValueError("Must have target or grp_id") -class LongMsgResult(ProtoStruct): - action: "LongMsgAction" = proto_field(2) + +class LongMsgActionBody(ProtoStruct): + action_list: list[MsgPushBody] = proto_field(1) class LongMsgAction(ProtoStruct): action_command: str = proto_field(1) - action_data: "LongMsgActionBody" = proto_field(2) + action_data: LongMsgActionBody = proto_field(2) -class LongMsgActionBody(ProtoStruct): - action_list: list[MsgPushBody] = proto_field(1) +class LongMsgResult(ProtoStruct): + action: LongMsgAction = proto_field(2) diff --git a/lagrange/pb/message/msg.py b/lagrange/pb/message/msg.py index 25e6d8b..ae5c42b 100644 --- a/lagrange/pb/message/msg.py +++ b/lagrange/pb/message/msg.py @@ -7,5 +7,5 @@ class Message(ProtoStruct): body: Optional[RichText] = proto_field(1, default=None) - buf2: bytes = proto_field(2, default=b"") - buf3: bytes = proto_field(3, default=b"") + buf2: Optional[bytes] = proto_field(2, default=None) + buf3: Optional[bytes] = proto_field(3, default=None) From ac47e79cbf4dc6c74e72a40e68f1cebc9b37a2e5 Mon Sep 17 00:00:00 2001 From: Decrabbit Date: Wed, 13 Nov 2024 22:08:06 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E9=98=BF=E5=B7=B4=E9=98=BF=E5=B7=B4?= =?UTF-8?q?=EF=BC=8C=E5=8F=91=E9=80=81=E4=BF=AE=E5=A4=8D=E3=80=82=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E5=8F=AF=E8=83=BD=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lagrange/client/client.py | 30 +++++++++++++++++-- lagrange/client/message/decoder.py | 14 ++++++++- lagrange/client/message/elems.py | 26 +++++++++++++++- lagrange/client/message/encoder.py | 48 ++++++++++++++++-------------- lagrange/pb/message/longmsg.py | 32 ++++++++++++++++++-- 5 files changed, 120 insertions(+), 30 deletions(-) diff --git a/lagrange/client/client.py b/lagrange/client/client.py index aed19eb..35835d2 100644 --- a/lagrange/client/client.py +++ b/lagrange/client/client.py @@ -1,3 +1,4 @@ +import gzip import os import struct import asyncio @@ -15,6 +16,7 @@ from collections.abc import Coroutine from lagrange.info import AppInfo, DeviceInfo, SigInfo +from lagrange.pb.message.longmsg import LongMsgResult, RecvLongMsgReq, RecvLongMsgRsp from lagrange.pb.message.msg_push import MsgPushBody from lagrange.pb.message.send import SendMsgRsp from lagrange.pb.service.comm import ( @@ -70,7 +72,7 @@ from .events.group import GroupMessage from .events.service import ClientOnline, ClientOffline from .highway import HighWaySession -from .message.decoder import parse_grp_msg +from .message.decoder import parse_grp_msg, parse_msg_new from .message.elems import Audio, Image, MulitMsg from .message.encoder import _get_mulitmsg_resid, build_message from .message.types import Element @@ -227,10 +229,10 @@ async def send_grp_forward_msg(self, forward_msg: MulitMsg, grp_id: int): if result.ret_code: raise AssertionError(result.ret_code, result.err_msg) return result.seq - + async def send_friend_forward_msg(self, forward_msg: MulitMsg, uid: str): forward_msg.resid = await _get_mulitmsg_resid(self, forward_msg, target=uid) - result = await self._send_msg_raw({1: (await build_message([forward_msg])).encode()},uid=uid) + result = await self._send_msg_raw({1: (await build_message([forward_msg])).encode()}, uid=uid) if result.ret_code: raise AssertionError(result.ret_code, result.err_msg) return result.seq @@ -623,3 +625,25 @@ async def get_rkey(self) -> tuple[str, str]: rsp = await self.send_oidb_svc(0x9067, 202, proto_encode(body), True) temp = proto_decode(rsp.data).into((4, 1), dict[int, list[bytes]]) return temp[0][1].decode(), temp[1][1].decode() + + async def get_forward_msg(self, res_id: str) -> list[list[Element]]: + """ + res_id: from MultiMsg + """ + ret: list[list[Element]] = [] + rsp = RecvLongMsgRsp.decode( + ( + await self.send_uni_packet( + "trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg", + RecvLongMsgReq.build(self.uid, res_id).encode(), + ) + ).data + ) + payload = gzip.decompress(rsp.result.payload) + awa = LongMsgResult.decode(payload) + for msg in awa.action: + for elem in msg.action_data.action_list: + ret.append(list(await parse_msg_new(self, elem))) + # 对,但是不是很对的解析 + # 顺序对了,但是顺序不对 + return ret # TODO: retrun MulitMsg diff --git a/lagrange/client/message/decoder.py b/lagrange/client/message/decoder.py index 4f5337d..6562903 100644 --- a/lagrange/client/message/decoder.py +++ b/lagrange/client/message/decoder.py @@ -1,4 +1,5 @@ import json +from xml.dom import minidom import zlib from typing import TYPE_CHECKING, cast, Literal, Union from collections.abc import Sequence @@ -218,7 +219,18 @@ async def parse_msg_new( content = zlib.decompress(jr[1:]) else: content = jr[1:] - msg_chain.append(elems.Service(id=sid, raw=content)) + if sid == 35: + # msg_chain.append(elems.MultiMsg(res_id=service)) + root: minidom.Document = minidom.parseString(content) + msg_elem: minidom.Element = root.getElementsByTagName("msg")[0] + return [ + elems.MulitMsg( + msg_elem.getAttribute("m_fileName"), + resid=msg_elem.getAttribute("m_resid"), + ) + ] + else: + msg_chain.append(elems.Service(id=sid, raw=content)) ignore_next = True elif raw.open_data: msg_chain.append(elems.Raw(data=raw.open_data.data)) diff --git a/lagrange/client/message/elems.py b/lagrange/client/message/elems.py index a749f3c..2387320 100644 --- a/lagrange/client/message/elems.py +++ b/lagrange/client/message/elems.py @@ -113,6 +113,10 @@ class AtAll(BaseElem): def display(self) -> str: return self.text + @property + def raw_text(self) -> str: + return self.text + @dataclass class At(BaseElem): @@ -140,6 +144,10 @@ class Image(CompatibleText, MediaInfo): is_emoji: bool display_name: str + @property + def raw_text(self) -> str: + return "[图片]" + @property def display(self) -> str: return self.display_name @@ -152,6 +160,10 @@ class Video(CompatibleText, MediaInfo): time: int file_key: str = field(repr=True) + @property + def raw_text(self) -> str: + return "[视频]" + @property def display(self) -> str: return f"[video:{self.width}x{self.height},{self.time}s]" @@ -162,6 +174,10 @@ class Audio(CompatibleText, MediaInfo): time: int file_key: str = field(repr=True) + @property + def raw_text(self) -> str: + return "[语音]" + @property def display(self) -> str: return f"[audio:{self.time}]" @@ -221,6 +237,10 @@ def url(self) -> str: def display(self) -> str: return f"[marketface:{self.name}]" + @property + def raw_text(self) -> str: + return "[动画表情]" + @dataclass class File(CompatibleText): @@ -350,7 +370,7 @@ class ForwardNode(BaseElem): content: list["Element"] sender_uin: int - sender_nick: str = "QQ用户" + sender_nick: str = "" sender_avatar_url: str = "" timestamp: int = field(default_factory=lambda: int(time.time())) @@ -364,3 +384,7 @@ class MulitMsg(BaseElem): @property def display(self) -> str: return f"[forward:{self.resid}]" + + @property + def raw_text(self) -> str: + return "[聊天记录]" diff --git a/lagrange/client/message/encoder.py b/lagrange/client/message/encoder.py index 9c62853..1041ea6 100644 --- a/lagrange/client/message/encoder.py +++ b/lagrange/client/message/encoder.py @@ -259,30 +259,32 @@ async def build_forward_msg( ) -> LongMsgResult: start_seq = random.randint(1000000, 9999999) return LongMsgResult( - action=LongMsgAction( - action_command="MultiMsg", - action_data=LongMsgActionBody( - action_list=[ - MsgPushBody( - response_head=ResponseHead( - from_uin=node.sender_uin, rsp_grp=Grp(sender_name=node.sender_nick, f5=2) - ), - content_head=ContentHead( - type=82, - random=random.randint(100000000, 2147483647), - seq=seq, - timestamp=node.timestamp, - forward=Forward( - custom_flag=b"666" if node.sender_nick or node.sender_avatar_url else b"", - avatar_url=node.sender_avatar_url, + action=[ + LongMsgAction( + action_command="MultiMsg", + action_data=LongMsgActionBody( + action_list=[ + MsgPushBody( + response_head=ResponseHead( + from_uin=node.sender_uin, rsp_grp=Grp(sender_name=node.sender_nick, f5=2) ), - ), - message=Message(body=await build_message(node.content, forward_func=forward_func)), - ) - for seq, node in enumerate(forword_msg.messages, start_seq) - ] - ), - ) + content_head=ContentHead( + type=82, + random=random.randint(100000000, 2147483647), + seq=seq, + timestamp=node.timestamp, + forward=Forward( + custom_flag=b"666" if node.sender_nick or node.sender_avatar_url else b"", + avatar_url=node.sender_avatar_url, + ), + ), + message=Message(body=await build_message(node.content, forward_func=forward_func)), + ) + for seq, node in enumerate(forword_msg.messages, start_seq) + ] + ), + ) + ] ) diff --git a/lagrange/pb/message/longmsg.py b/lagrange/pb/message/longmsg.py index 6349682..1a15876 100644 --- a/lagrange/pb/message/longmsg.py +++ b/lagrange/pb/message/longmsg.py @@ -58,9 +58,37 @@ class LongMsgActionBody(ProtoStruct): class LongMsgAction(ProtoStruct): - action_command: str = proto_field(1) + action_command: str = proto_field(1) # 接收时也可能是uniseq action_data: LongMsgActionBody = proto_field(2) class LongMsgResult(ProtoStruct): - action: LongMsgAction = proto_field(2) + action: list[LongMsgAction] = proto_field(2) + + +class RecvLongMsgInfo(ProtoStruct): + uid: MulitMsgProperty = proto_field(1) + res_id: str = proto_field(2) + acquire: bool = proto_field(3, default=True) + + +class RecvLongMsgReq(ProtoStruct): + info: RecvLongMsgInfo = proto_field(1) + settings: LongMsgCfg = proto_field(15) + + @classmethod + def build(cls, uid: str, res_id: str): + return cls( + info=RecvLongMsgInfo(uid=MulitMsgProperty(value=uid), res_id=res_id), + settings=LongMsgCfg(f1=2, f2=0, f3=0, f4=0), + ) + + +class RecvLongMsgResult(ProtoStruct): + res_id: str = proto_field(3) + payload: bytes = proto_field(4) + + +class RecvLongMsgRsp(ProtoStruct): + result: RecvLongMsgResult = proto_field(1) + settings: LongMsgCfg = proto_field(15)