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 }