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

     1  package client
     2  
     3  import (
     4  	"crypto/rand"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/fumiama/imgsz"
    11  	"github.com/pkg/errors"
    12  
    13  	"github.com/Mrs4s/MiraiGo/binary"
    14  	"github.com/Mrs4s/MiraiGo/client/internal/highway"
    15  	"github.com/Mrs4s/MiraiGo/client/internal/network"
    16  	"github.com/Mrs4s/MiraiGo/client/pb/cmd0x388"
    17  	highway2 "github.com/Mrs4s/MiraiGo/client/pb/highway"
    18  	"github.com/Mrs4s/MiraiGo/client/pb/oidb"
    19  	"github.com/Mrs4s/MiraiGo/internal/proto"
    20  	"github.com/Mrs4s/MiraiGo/message"
    21  	"github.com/Mrs4s/MiraiGo/utils"
    22  )
    23  
    24  func init() {
    25  	decoders["ImgStore.GroupPicUp"] = decodeGroupImageStoreResponse
    26  	decoders["ImgStore.GroupPicDown"] = decodeGroupImageDownloadResponse
    27  	decoders["OidbSvc.0xe07_0"] = decodeImageOcrResponse
    28  }
    29  
    30  var imgWaiter = utils.NewUploadWaiter()
    31  
    32  type imageUploadResponse struct {
    33  	UploadKey     []byte
    34  	UploadIp      []uint32
    35  	UploadPort    []uint32
    36  	Width         int32
    37  	Height        int32
    38  	Message       string
    39  	DownloadIndex string
    40  	ResourceId    string
    41  	FileId        int64
    42  	ResultCode    int32
    43  	IsExists      bool
    44  }
    45  
    46  func (c *QQClient) UploadImage(target message.Source, img io.ReadSeeker) (message.IMessageElement, error) {
    47  	switch target.SourceType {
    48  	case message.SourceGroup, message.SourceGuildChannel, message.SourceGuildDirect:
    49  		return c.uploadGroupOrGuildImage(target, img)
    50  	case message.SourcePrivate:
    51  		return c.uploadPrivateImage(target.PrimaryID, img, 0)
    52  	default:
    53  		return nil, errors.New("unsupported target type")
    54  	}
    55  }
    56  
    57  func (c *QQClient) uploadGroupOrGuildImage(target message.Source, img io.ReadSeeker) (message.IMessageElement, error) {
    58  	_, _ = img.Seek(0, io.SeekStart) // safe
    59  	fh, length := utils.ComputeMd5AndLength(img)
    60  	_, _ = img.Seek(0, io.SeekStart)
    61  
    62  	key := string(fh)
    63  	imgWaiter.Wait(key)
    64  	defer imgWaiter.Done(key)
    65  
    66  	cmd := int32(2)
    67  	ext := EmptyBytes
    68  	if target.SourceType != message.SourceGroup { // guild
    69  		cmd = 83
    70  		ext = proto.DynamicMessage{
    71  			11: target.PrimaryID,
    72  			12: target.SecondaryID,
    73  		}.Encode()
    74  	}
    75  
    76  	var r any
    77  	var err error
    78  	var input highway.Transaction
    79  	switch target.SourceType {
    80  	case message.SourceGroup:
    81  		r, err = c.sendAndWait(c.buildGroupImageStorePacket(target.PrimaryID, fh, int32(length)))
    82  	case message.SourceGuildChannel, message.SourceGuildDirect:
    83  		r, err = c.sendAndWait(c.buildGuildImageStorePacket(uint64(target.PrimaryID), uint64(target.SecondaryID), fh, uint64(length)))
    84  	default:
    85  		return nil, errors.Errorf("unsupported target type %v", target.SourceType)
    86  	}
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	rsp := r.(*imageUploadResponse)
    91  	if rsp.ResultCode != 0 {
    92  		return nil, errors.New(rsp.Message)
    93  	}
    94  	if rsp.IsExists {
    95  		goto ok
    96  	}
    97  	if c.highwaySession.AddrLength() == 0 {
    98  		for i, addr := range rsp.UploadIp {
    99  			c.highwaySession.AppendAddr(addr, rsp.UploadPort[i])
   100  		}
   101  	}
   102  
   103  	input = highway.Transaction{
   104  		CommandID: cmd,
   105  		Body:      img,
   106  		Size:      length,
   107  		Sum:       fh,
   108  		Ticket:    rsp.UploadKey,
   109  		Ext:       ext,
   110  	}
   111  	_, err = c.highwaySession.Upload(input)
   112  	if err != nil {
   113  		return nil, errors.Wrap(err, "upload failed")
   114  	}
   115  ok:
   116  	_, _ = img.Seek(0, io.SeekStart)
   117  	i, t, _ := imgsz.DecodeSize(img)
   118  	var imageType int32 = 1000
   119  	if t == "gif" {
   120  		imageType = 2000
   121  	}
   122  	width := int32(i.Width)
   123  	height := int32(i.Height)
   124  	if err != nil && target.SourceType != message.SourceGroup {
   125  		c.warning("warning: decode image error: %v. this image will be displayed by wrong size in pc guild client", err)
   126  		width = 200
   127  		height = 200
   128  	}
   129  	if target.SourceType == message.SourceGroup {
   130  		return message.NewGroupImage(
   131  			binary.CalculateImageResourceId(fh),
   132  			fh, rsp.FileId, int32(length),
   133  			int32(i.Width), int32(i.Height), imageType,
   134  		), nil
   135  	}
   136  	return &message.GuildImageElement{
   137  		FileId:        rsp.FileId,
   138  		FilePath:      fmt.Sprintf("%x.jpg", fh),
   139  		Size:          int32(length),
   140  		DownloadIndex: rsp.DownloadIndex,
   141  		Width:         width,
   142  		Height:        height,
   143  		ImageType:     imageType,
   144  		Md5:           fh,
   145  	}, nil
   146  }
   147  
   148  func (c *QQClient) GetGroupImageDownloadUrl(fileId, groupCode int64, fileMd5 []byte) (string, error) {
   149  	i, err := c.sendAndWait(c.buildGroupImageDownloadPacket(fileId, groupCode, fileMd5))
   150  	if err != nil {
   151  		return "", err
   152  	}
   153  	return i.(string), nil
   154  }
   155  
   156  func (c *QQClient) uploadPrivateImage(target int64, img io.ReadSeeker, count int) (message.IMessageElement, error) {
   157  	_, _ = img.Seek(0, io.SeekStart)
   158  	count++
   159  	fh, length := utils.ComputeMd5AndLength(img)
   160  	_, _ = img.Seek(0, io.SeekStart)
   161  	i, _, _ := imgsz.DecodeSize(img)
   162  	_, _ = img.Seek(0, io.SeekStart)
   163  	width := int32(i.Width)
   164  	height := int32(i.Height)
   165  	e, err := c.QueryFriendImage(target, fh, int32(length))
   166  	if errors.Is(err, ErrNotExists) {
   167  		groupSource := message.Source{
   168  			SourceType: message.SourceGroup,
   169  			PrimaryID:  target,
   170  		}
   171  		// use group highway upload and query again for image id.
   172  		if _, err = c.uploadGroupOrGuildImage(groupSource, img); err != nil {
   173  			return nil, err
   174  		}
   175  		if count >= 5 {
   176  			return e, nil
   177  		}
   178  		return c.uploadPrivateImage(target, img, count)
   179  	}
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	e.Height = height
   184  	e.Width = width
   185  	return e, nil
   186  }
   187  
   188  func (c *QQClient) ImageOcr(img any) (*OcrResponse, error) {
   189  	url := ""
   190  	switch e := img.(type) {
   191  	case *message.GroupImageElement:
   192  		url = e.Url
   193  		if b, err := utils.HTTPGetReadCloser(e.Url, ""); err == nil {
   194  			if url, err = c.uploadOcrImage(b, e.Size, e.Md5); err != nil {
   195  				url = e.Url
   196  			}
   197  			_ = b.Close()
   198  		}
   199  		rsp, err := c.sendAndWait(c.buildImageOcrRequestPacket(url, fmt.Sprintf("%X", e.Md5), e.Size, e.Width, e.Height))
   200  		if err != nil {
   201  			return nil, err
   202  		}
   203  		return rsp.(*OcrResponse), nil
   204  	}
   205  	return nil, errors.New("image error")
   206  }
   207  
   208  func (c *QQClient) QueryGroupImage(groupCode int64, hash []byte, size int32) (*message.GroupImageElement, error) {
   209  	r, err := c.sendAndWait(c.buildGroupImageStorePacket(groupCode, hash, size))
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  	rsp := r.(*imageUploadResponse)
   214  	if rsp.ResultCode != 0 {
   215  		return nil, errors.New(rsp.Message)
   216  	}
   217  	if rsp.IsExists {
   218  		return message.NewGroupImage(binary.CalculateImageResourceId(hash), hash, rsp.FileId, size, rsp.Width, rsp.Height, 1000), nil
   219  	}
   220  	return nil, errors.New("image does not exist")
   221  }
   222  
   223  func (c *QQClient) QueryFriendImage(target int64, hash []byte, size int32) (*message.FriendImageElement, error) {
   224  	i, err := c.sendAndWait(c.buildOffPicUpPacket(target, hash, size))
   225  	if err != nil {
   226  		return nil, err
   227  	}
   228  	rsp := i.(*imageUploadResponse)
   229  	if rsp.ResultCode != 0 {
   230  		return nil, errors.New(rsp.Message)
   231  	}
   232  	if !rsp.IsExists {
   233  		return &message.FriendImageElement{
   234  			ImageId: rsp.ResourceId,
   235  			Md5:     hash,
   236  			Size:    size,
   237  			Url:     "https://c2cpicdw.qpic.cn/offpic_new/0/" + rsp.ResourceId + "/0?term=2",
   238  		}, errors.WithStack(ErrNotExists)
   239  	}
   240  	return &message.FriendImageElement{
   241  		ImageId: rsp.ResourceId,
   242  		Md5:     hash,
   243  		Url:     "https://c2cpicdw.qpic.cn/offpic_new/0/" + rsp.ResourceId + "/0?term=2",
   244  		Size:    size,
   245  		Height:  rsp.Height,
   246  		Width:   rsp.Width,
   247  	}, nil
   248  }
   249  
   250  // ImgStore.GroupPicUp
   251  func (c *QQClient) buildGroupImageStorePacket(groupCode int64, md5 []byte, size int32) (uint16, []byte) {
   252  	name := utils.RandomString(16) + ".gif"
   253  	req := &cmd0x388.D388ReqBody{
   254  		NetType: proto.Uint32(3),
   255  		Subcmd:  proto.Uint32(1),
   256  		TryupImgReq: []*cmd0x388.TryUpImgReq{
   257  			{
   258  				GroupCode:    proto.Uint64(uint64(groupCode)),
   259  				SrcUin:       proto.Uint64(uint64(c.Uin)),
   260  				FileMd5:      md5,
   261  				FileSize:     proto.Uint64(uint64(size)),
   262  				FileName:     utils.S2B(name),
   263  				SrcTerm:      proto.Uint32(5),
   264  				PlatformType: proto.Uint32(9),
   265  				BuType:       proto.Uint32(1),
   266  				PicType:      proto.Uint32(1000),
   267  				BuildVer:     utils.S2B("8.2.7.4410"),
   268  				AppPicType:   proto.Uint32(1006),
   269  				FileIndex:    EmptyBytes,
   270  				TransferUrl:  EmptyBytes,
   271  			},
   272  		},
   273  		Extension: EmptyBytes,
   274  	}
   275  	payload, _ := proto.Marshal(req)
   276  	return c.uniPacket("ImgStore.GroupPicUp", payload)
   277  }
   278  
   279  func (c *QQClient) buildGroupImageDownloadPacket(fileId, groupCode int64, fileMd5 []byte) (uint16, []byte) {
   280  	req := &cmd0x388.D388ReqBody{
   281  		NetType: proto.Uint32(3),
   282  		Subcmd:  proto.Uint32(2),
   283  		GetimgUrlReq: []*cmd0x388.GetImgUrlReq{
   284  			{
   285  				FileId:          proto.Uint64(0), // index
   286  				DstUin:          proto.Uint64(uint64(c.Uin)),
   287  				GroupCode:       proto.Uint64(uint64(groupCode)),
   288  				FileMd5:         fileMd5,
   289  				PicUpTimestamp:  proto.Uint32(uint32(time.Now().Unix())),
   290  				Fileid:          proto.Uint64(uint64(fileId)),
   291  				UrlFlag:         proto.Uint32(8),
   292  				UrlType:         proto.Uint32(3),
   293  				ReqPlatformType: proto.Uint32(9),
   294  				ReqTerm:         proto.Uint32(5),
   295  				InnerIp:         proto.Uint32(0),
   296  			},
   297  		},
   298  	}
   299  	payload, _ := proto.Marshal(req)
   300  	return c.uniPacket("ImgStore.GroupPicDown", payload)
   301  }
   302  
   303  func (c *QQClient) uploadOcrImage(img io.Reader, size int32, sum []byte) (string, error) {
   304  	r := make([]byte, 16)
   305  	rand.Read(r)
   306  	ext, _ := proto.Marshal(&highway2.CommFileExtReq{
   307  		ActionType: proto.Uint32(0),
   308  		Uuid:       binary.GenUUID(r),
   309  	})
   310  
   311  	rsp, err := c.highwaySession.Upload(highway.Transaction{
   312  		CommandID: 76,
   313  		Body:      img,
   314  		Size:      int64(size),
   315  		Sum:       sum,
   316  		Ticket:    c.highwaySession.SigSession,
   317  		Ext:       ext,
   318  	})
   319  	if err != nil {
   320  		return "", errors.Wrap(err, "upload ocr image error")
   321  	}
   322  	rspExt := highway2.CommFileExtRsp{}
   323  	if err = proto.Unmarshal(rsp, &rspExt); err != nil {
   324  		return "", errors.Wrap(err, "error unmarshal highway resp")
   325  	}
   326  	return string(rspExt.DownloadUrl), nil
   327  }
   328  
   329  // OidbSvc.0xe07_0
   330  func (c *QQClient) buildImageOcrRequestPacket(url, md5 string, size, weight, height int32) (uint16, []byte) {
   331  	body := &oidb.DE07ReqBody{
   332  		Version:  1,
   333  		Entrance: 3,
   334  		OcrReqBody: &oidb.OCRReqBody{
   335  			ImageUrl:              url,
   336  			OriginMd5:             md5,
   337  			AfterCompressMd5:      md5,
   338  			AfterCompressFileSize: size,
   339  			AfterCompressWeight:   weight,
   340  			AfterCompressHeight:   height,
   341  			IsCut:                 false,
   342  		},
   343  	}
   344  	b, _ := proto.Marshal(body)
   345  	payload := c.packOIDBPackage(3591, 0, b)
   346  	return c.uniPacket("OidbSvc.0xe07_0", payload)
   347  }
   348  
   349  // ImgStore.GroupPicUp
   350  func decodeGroupImageStoreResponse(_ *QQClient, packet *network.Packet) (any, error) {
   351  	pkt := cmd0x388.D388RspBody{}
   352  	err := proto.Unmarshal(packet.Payload, &pkt)
   353  	if err != nil {
   354  		return nil, errors.Wrap(err, "failed to unmarshal protobuf message")
   355  	}
   356  	rsp := pkt.TryupImgRsp[0]
   357  	if rsp.Result.Unwrap() != 0 {
   358  		return &imageUploadResponse{
   359  			ResultCode: int32(rsp.Result.Unwrap()),
   360  			Message:    utils.B2S(rsp.FailMsg),
   361  		}, nil
   362  	}
   363  	if rsp.FileExit.Unwrap() {
   364  		if rsp.ImgInfo != nil {
   365  			return &imageUploadResponse{IsExists: true, FileId: int64(rsp.Fileid.Unwrap()), Width: int32(rsp.ImgInfo.FileWidth.Unwrap()), Height: int32(rsp.ImgInfo.FileHeight.Unwrap())}, nil
   366  		}
   367  		return &imageUploadResponse{IsExists: true, FileId: int64(rsp.Fileid.Unwrap())}, nil
   368  	}
   369  	return &imageUploadResponse{
   370  		FileId:     int64(rsp.Fileid.Unwrap()),
   371  		UploadKey:  rsp.UpUkey,
   372  		UploadIp:   rsp.UpIp,
   373  		UploadPort: rsp.UpPort,
   374  	}, nil
   375  }
   376  
   377  func decodeGroupImageDownloadResponse(_ *QQClient, pkt *network.Packet) (any, error) {
   378  	rsp := cmd0x388.D388RspBody{}
   379  	if err := proto.Unmarshal(pkt.Payload, &rsp); err != nil {
   380  		return nil, errors.Wrap(err, "unmarshal protobuf message error")
   381  	}
   382  	if len(rsp.GetimgUrlRsp) == 0 {
   383  		return nil, errors.New("response not found")
   384  	}
   385  	if len(rsp.GetimgUrlRsp[0].FailMsg) != 0 {
   386  		return nil, errors.New(utils.B2S(rsp.GetimgUrlRsp[0].FailMsg))
   387  	}
   388  	return fmt.Sprintf("https://%s%s", rsp.GetimgUrlRsp[0].DownDomain, rsp.GetimgUrlRsp[0].BigDownPara), nil
   389  }
   390  
   391  // OidbSvc.0xe07_0
   392  func decodeImageOcrResponse(_ *QQClient, pkt *network.Packet) (any, error) {
   393  	rsp := oidb.DE07RspBody{}
   394  	err := unpackOIDBPackage(pkt.Payload, &rsp)
   395  	if err != nil {
   396  		return nil, err
   397  	}
   398  	if rsp.Wording != "" {
   399  		if strings.Contains(rsp.Wording, "服务忙") {
   400  			return nil, errors.New("未识别到文本")
   401  		}
   402  		return nil, errors.New(rsp.Wording)
   403  	}
   404  	if rsp.RetCode != 0 {
   405  		return nil, errors.Errorf("server error, code: %v msg: %v", rsp.RetCode, rsp.ErrMsg)
   406  	}
   407  	texts := make([]*TextDetection, 0, len(rsp.OcrRspBody.TextDetections))
   408  	for _, text := range rsp.OcrRspBody.TextDetections {
   409  		points := make([]*Coordinate, 0, len(text.Polygon.Coordinates))
   410  		for _, c := range text.Polygon.Coordinates {
   411  			points = append(points, &Coordinate{
   412  				X: c.X,
   413  				Y: c.Y,
   414  			})
   415  		}
   416  		texts = append(texts, &TextDetection{
   417  			Text:        text.DetectedText,
   418  			Confidence:  text.Confidence,
   419  			Coordinates: points,
   420  		})
   421  	}
   422  	return &OcrResponse{
   423  		Texts:    texts,
   424  		Language: rsp.OcrRspBody.Language,
   425  	}, nil
   426  }