github.com/Mrs4s/MiraiGo@v0.0.0-20240226124653-54bdd873e3fe/client/multimsg.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"fmt"
     7  	"math"
     8  	"math/rand"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/pkg/errors"
    14  
    15  	"github.com/Mrs4s/MiraiGo/binary"
    16  	"github.com/Mrs4s/MiraiGo/client/internal/highway"
    17  	"github.com/Mrs4s/MiraiGo/client/internal/network"
    18  	"github.com/Mrs4s/MiraiGo/client/pb/longmsg"
    19  	"github.com/Mrs4s/MiraiGo/client/pb/msg"
    20  	"github.com/Mrs4s/MiraiGo/client/pb/multimsg"
    21  	"github.com/Mrs4s/MiraiGo/internal/proto"
    22  	"github.com/Mrs4s/MiraiGo/message"
    23  	"github.com/Mrs4s/MiraiGo/utils"
    24  )
    25  
    26  func init() {
    27  	decoders["MultiMsg.ApplyUp"] = decodeMultiApplyUpResponse
    28  	decoders["MultiMsg.ApplyDown"] = decodeMultiApplyDownResponse
    29  }
    30  
    31  // MultiMsg.ApplyUp
    32  func (c *QQClient) buildMultiApplyUpPacket(data, hash []byte, buType int32, groupUin int64) (uint16, []byte) {
    33  	req := &multimsg.MultiReqBody{
    34  		Subcmd:       1,
    35  		TermType:     5,
    36  		PlatformType: 9,
    37  		NetType:      3,
    38  		BuildVer:     "8.2.0.1296",
    39  		MultimsgApplyupReq: []*multimsg.MultiMsgApplyUpReq{
    40  			{
    41  				DstUin:  groupUin,
    42  				MsgSize: int64(len(data)),
    43  				MsgMd5:  hash,
    44  				MsgType: 3,
    45  			},
    46  		},
    47  		BuType: buType,
    48  	}
    49  	payload, _ := proto.Marshal(req)
    50  	return c.uniPacket("MultiMsg.ApplyUp", payload)
    51  }
    52  
    53  // MultiMsg.ApplyUp
    54  func decodeMultiApplyUpResponse(_ *QQClient, pkt *network.Packet) (any, error) {
    55  	body := multimsg.MultiRspBody{}
    56  	if err := proto.Unmarshal(pkt.Payload, &body); err != nil {
    57  		return nil, errors.Wrap(err, "failed to unmarshal protobuf message")
    58  	}
    59  	if len(body.MultimsgApplyupRsp) == 0 {
    60  		return nil, errors.New("rsp is empty")
    61  	}
    62  	rsp := body.MultimsgApplyupRsp[0]
    63  	switch rsp.Result {
    64  	case 0:
    65  		return rsp, nil
    66  	case 193:
    67  		return nil, errors.New("too large")
    68  	}
    69  	return nil, errors.Errorf("unexpected multimsg apply up response: %d", rsp.Result)
    70  }
    71  
    72  // MultiMsg.ApplyDown
    73  func (c *QQClient) buildMultiApplyDownPacket(resID string) (uint16, []byte) {
    74  	req := &multimsg.MultiReqBody{
    75  		Subcmd:       2,
    76  		TermType:     5,
    77  		PlatformType: 9,
    78  		NetType:      3,
    79  		BuildVer:     "8.2.0.1296",
    80  		MultimsgApplydownReq: []*multimsg.MultiMsgApplyDownReq{
    81  			{
    82  				MsgResid: []byte(resID),
    83  				MsgType:  3,
    84  			},
    85  		},
    86  		BuType:         2,
    87  		ReqChannelType: 2,
    88  	}
    89  	payload, _ := proto.Marshal(req)
    90  	return c.uniPacket("MultiMsg.ApplyDown", payload)
    91  }
    92  
    93  // MultiMsg.ApplyDown
    94  func decodeMultiApplyDownResponse(_ *QQClient, pkt *network.Packet) (any, error) {
    95  	body := multimsg.MultiRspBody{}
    96  	if err := proto.Unmarshal(pkt.Payload, &body); err != nil {
    97  		return nil, errors.Wrap(err, "failed to unmarshal protobuf message")
    98  	}
    99  	if len(body.MultimsgApplydownRsp) == 0 {
   100  		return nil, errors.New("message not found")
   101  	}
   102  	rsp := body.MultimsgApplydownRsp[0]
   103  
   104  	if rsp.ThumbDownPara == nil {
   105  		return nil, errors.New("message not found")
   106  	}
   107  
   108  	var prefix string
   109  	if rsp.MsgExternInfo != nil && rsp.MsgExternInfo.ChannelType == 2 {
   110  		prefix = "https://ssl.htdata.qq.com"
   111  	} else {
   112  		ma := body.MultimsgApplydownRsp[0]
   113  		if len(rsp.Uint32DownIp) == 0 || len(ma.Uint32DownPort) == 0 {
   114  			return nil, errors.New("message not found")
   115  		}
   116  		prefix = fmt.Sprintf("http://%s:%d", binary.UInt32ToIPV4Address(uint32(rsp.Uint32DownIp[0])), ma.Uint32DownPort[0])
   117  	}
   118  	b, err := utils.HttpGetBytes(fmt.Sprintf("%s%s", prefix, string(rsp.ThumbDownPara)), "")
   119  	if err != nil {
   120  		return nil, errors.Wrap(err, "failed to download by multi apply down")
   121  	}
   122  	if b[0] != 40 {
   123  		return nil, errors.New("unexpected body data")
   124  	}
   125  	tea := binary.NewTeaCipher(body.MultimsgApplydownRsp[0].MsgKey)
   126  	r := binary.NewReader(b[1:])
   127  	i1 := r.ReadInt32()
   128  	i2 := r.ReadInt32()
   129  	if i1 > 0 {
   130  		r.ReadBytes(int(i1)) // im msg head
   131  	}
   132  	data := tea.Decrypt(r.ReadBytes(int(i2)))
   133  	lb := longmsg.LongRspBody{}
   134  	if err = proto.Unmarshal(data, &lb); err != nil {
   135  		return nil, errors.Wrap(err, "failed to unmarshal protobuf message")
   136  	}
   137  	msgContent := lb.MsgDownRsp[0].MsgContent
   138  	if msgContent == nil {
   139  		return nil, errors.New("message content is empty")
   140  	}
   141  	uc := binary.GZipUncompress(msgContent)
   142  	mt := msg.PbMultiMsgTransmit{}
   143  	if err = proto.Unmarshal(uc, &mt); err != nil {
   144  		return nil, errors.Wrap(err, "failed to unmarshal protobuf message")
   145  	}
   146  	return &mt, nil
   147  }
   148  
   149  type forwardMsgLinker struct {
   150  	items map[string]*msg.PbMultiMsgItem
   151  }
   152  
   153  func (l *forwardMsgLinker) link(name string) *message.ForwardMessage {
   154  	item := l.items[name]
   155  	if item == nil {
   156  		return nil
   157  	}
   158  	nodes := make([]*message.ForwardNode, 0, len(item.Buffer.Msg))
   159  	for _, m := range item.Buffer.Msg {
   160  		name := m.Head.FromNick.Unwrap()
   161  		if m.Head.MsgType.Unwrap() == 82 && m.Head.GroupInfo != nil {
   162  			name = m.Head.GroupInfo.GroupCard.Unwrap()
   163  		}
   164  
   165  		msgElems := message.ParseMessageElems(m.Body.RichText.Elems)
   166  		for i, elem := range msgElems {
   167  			if forward, ok := elem.(*message.ForwardElement); ok {
   168  				if forward.FileName != "" {
   169  					msgElems[i] = l.link(forward.FileName) // 递归处理嵌套转发
   170  				}
   171  			}
   172  		}
   173  
   174  		gid := int64(0) // 给群号一个缺省值0,防止在读合并转发的私聊内容时候会报错
   175  		if m.Head.GroupInfo != nil {
   176  			gid = m.Head.GroupInfo.GroupCode.Unwrap()
   177  		}
   178  		nodes = append(nodes, &message.ForwardNode{
   179  			GroupId:    gid,
   180  			SenderId:   m.Head.FromUin.Unwrap(),
   181  			SenderName: name,
   182  			Time:       m.Head.MsgTime.Unwrap(),
   183  			Message:    msgElems,
   184  		})
   185  	}
   186  	return &message.ForwardMessage{Nodes: nodes}
   187  }
   188  
   189  func (c *QQClient) GetForwardMessage(resID string) *message.ForwardMessage {
   190  	m := c.DownloadForwardMessage(resID)
   191  	if m == nil {
   192  		return nil
   193  	}
   194  	linker := forwardMsgLinker{
   195  		items: make(map[string]*msg.PbMultiMsgItem),
   196  	}
   197  	for _, item := range m.Items {
   198  		linker.items[item.FileName.Unwrap()] = item
   199  	}
   200  	return linker.link("MultiMsg")
   201  }
   202  
   203  func (c *QQClient) DownloadForwardMessage(resId string) *message.ForwardElement {
   204  	i, err := c.sendAndWait(c.buildMultiApplyDownPacket(resId))
   205  	if err != nil {
   206  		return nil
   207  	}
   208  	multiMsg := i.(*msg.PbMultiMsgTransmit)
   209  	if multiMsg.PbItemList == nil {
   210  		return nil
   211  	}
   212  	var pv bytes.Buffer
   213  	for i := 0; i < int(math.Min(4, float64(len(multiMsg.Msg)))); i++ {
   214  		m := multiMsg.Msg[i]
   215  		sender := m.Head.FromNick.Unwrap()
   216  		if m.Head.MsgType.Unwrap() == 82 && m.Head.GroupInfo != nil {
   217  			sender = m.Head.GroupInfo.GroupCard.Unwrap()
   218  		}
   219  		brief := message.ToReadableString(message.ParseMessageElems(multiMsg.Msg[i].Body.RichText.Elems))
   220  		fmt.Fprintf(&pv, `<title size="26" color="#777777">%s: %s</title>`, sender, brief)
   221  	}
   222  	return genForwardTemplate(
   223  		resId, pv.String(),
   224  		fmt.Sprintf("查看 %d 条转发消息", len(multiMsg.Msg)),
   225  		time.Now().UnixNano(),
   226  		multiMsg.PbItemList,
   227  	)
   228  }
   229  
   230  func forwardDisplay(resID, fileName, preview, summary string) string {
   231  	sb := strings.Builder{}
   232  	sb.WriteString(`<?xml version='1.0' encoding='UTF-8'?><msg serviceID="35" templateID="1" action="viewMultiMsg" brief="[聊天记录]" `)
   233  	if resID != "" {
   234  		sb.WriteString(`m_resid="`)
   235  		sb.WriteString(resID)
   236  		sb.WriteString("\" ")
   237  	}
   238  	sb.WriteString(`m_fileName="`)
   239  	sb.WriteString(fileName)
   240  	sb.WriteString(`" tSum="3" sourceMsgId="0" url="" flag="3" adverSign="0" multiMsgFlag="0"><item layout="1"><title color="#000000" size="34">群聊的聊天记录</title> `)
   241  	sb.WriteString(preview)
   242  	sb.WriteString(`<hr></hr><summary size="26" color="#808080">`)
   243  	sb.WriteString(summary)
   244  	// todo: 私聊的聊天记录?
   245  	sb.WriteString(`</summary></item><source name="聊天记录"></source></msg>`)
   246  	return sb.String()
   247  }
   248  
   249  func (c *QQClient) NewForwardMessageBuilder(groupCode int64) *ForwardMessageBuilder {
   250  	return &ForwardMessageBuilder{
   251  		c:         c,
   252  		groupCode: groupCode,
   253  	}
   254  }
   255  
   256  type ForwardMessageBuilder struct {
   257  	c         *QQClient
   258  	groupCode int64
   259  	objs      []*msg.PbMultiMsgItem
   260  }
   261  
   262  // NestedNode 返回一个嵌套转发节点,其内容将会被 Builder 重定位
   263  func (builder *ForwardMessageBuilder) NestedNode() *message.ForwardElement {
   264  	filename := strconv.FormatInt(time.Now().UnixNano(), 10) // 大概率不会重复
   265  	return &message.ForwardElement{FileName: filename}
   266  }
   267  
   268  // Link 将真实的消息内容填充 reloc
   269  func (builder *ForwardMessageBuilder) Link(reloc *message.ForwardElement, fmsg *message.ForwardMessage) {
   270  	seq := builder.c.nextGroupSeq()
   271  	m := fmsg.PackForwardMessage(seq, rand.Int31(), builder.groupCode)
   272  	builder.objs = append(builder.objs, &msg.PbMultiMsgItem{
   273  		FileName: proto.String(reloc.FileName),
   274  		Buffer: &msg.PbMultiMsgNew{
   275  			Msg: m,
   276  		},
   277  	})
   278  	reloc.Content = forwardDisplay("", reloc.FileName, fmsg.Preview(), fmt.Sprintf("查看 %d 条转发消息", fmsg.Length()))
   279  }
   280  
   281  // Main 最外层的转发消息, 调用该方法后即上传消息
   282  func (builder *ForwardMessageBuilder) Main(m *message.ForwardMessage) *message.ForwardElement {
   283  	if m.Length() > 200 {
   284  		return nil
   285  	}
   286  	c := builder.c
   287  	seq := c.nextGroupSeq()
   288  	fm := m.PackForwardMessage(seq, rand.Int31(), builder.groupCode)
   289  	const filename = "MultiMsg"
   290  	builder.objs = append(builder.objs, &msg.PbMultiMsgItem{
   291  		FileName: proto.String(filename),
   292  		Buffer: &msg.PbMultiMsgNew{
   293  			Msg: fm,
   294  		},
   295  	})
   296  	trans := &msg.PbMultiMsgTransmit{
   297  		Msg:        fm,
   298  		PbItemList: builder.objs,
   299  	}
   300  	b, _ := proto.Marshal(trans)
   301  	data := binary.GZipCompress(b)
   302  	hash := md5.Sum(data)
   303  	rsp, body, err := c.multiMsgApplyUp(builder.groupCode, data, hash[:], 2)
   304  	if err != nil {
   305  		return nil
   306  	}
   307  	content := forwardDisplay(rsp.MsgResid, utils.RandomString(32), m.Preview(), fmt.Sprintf("查看 %d 条转发消息", m.Length()))
   308  	bodyHash := md5.Sum(body)
   309  	input := highway.Transaction{
   310  		CommandID: 27,
   311  		Ticket:    rsp.MsgSig,
   312  		Body:      bytes.NewReader(body),
   313  		Sum:       bodyHash[:],
   314  		Size:      int64(len(body)),
   315  	}
   316  	_, err = c.highwaySession.Upload(input)
   317  	if err != nil {
   318  		return nil
   319  	}
   320  	return &message.ForwardElement{
   321  		FileName: filename,
   322  		Content:  content,
   323  		ResId:    rsp.MsgResid,
   324  	}
   325  }