From ca247a765e3bf8e24abafbf2d2e972f13dc6f57f Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Fri, 30 Aug 2024 01:25:02 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E5=88=9D=E6=AD=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=8A=E4=BC=A0=E8=A7=86=E9=A2=91=E8=87=B3=E7=BE=A4?= =?UTF-8?q?=E7=9B=B8=E5=86=8C=20-=20=E7=9B=AE=E5=89=8D=E4=BB=8D=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E8=8E=B7=E5=8F=96=E4=B8=8A=E4=BC=A0=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E7=9A=84=E8=A7=86=E9=A2=91=E7=9A=84URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/album.go | 450 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 362 insertions(+), 88 deletions(-) diff --git a/client/album.go b/client/album.go index f518d34..ceffbe5 100644 --- a/client/album.go +++ b/client/album.go @@ -6,17 +6,14 @@ import ( "encoding/json" "errors" "fmt" + "github.com/LagrangeDev/LagrangeGo/utils" + "github.com/LagrangeDev/LagrangeGo/utils/crypto" + "github.com/tidwall/gjson" "io" "mime/multipart" "net/http" "strconv" "time" - - "github.com/LagrangeDev/LagrangeGo/utils" - - "github.com/LagrangeDev/LagrangeGo/utils/crypto" - - "github.com/tidwall/gjson" ) const TimeLayout = "2006-01-02 15:04:05" @@ -71,13 +68,13 @@ func (c *QQClient) GetGroupAlbum(groupUin uint32) ([]*GroupAlbum, error) { return grpAlbumList, nil } -func (c *QQClient) getGroupAlbunUploadSession(groupUin uint32, fileName, albumId, albumName string, md5 []byte, size, gtk int) (*uploadOptions, error) { +func (c *QQClient) buildUploadSessionReq(param *uploadSessionParam) (*groupAlbumUploadReq, int64, error) { + timeStamp := time.Now().Unix() cookies, err := c.GetCookies("qzone.qq.com") if err != nil { - return nil, err + return nil, timeStamp, err } - timeStamp := time.Now().Unix() - reqBody := &groupAlbunUploadReq{ + reqBody := &groupAlbumUploadReq{ ControlReq: []controlReq{ { Uin: strconv.Itoa(int(c.Uin)), @@ -86,68 +83,137 @@ func (c *QQClient) getGroupAlbunUploadSession(groupUin uint32, fileName, albumId Data: cookies.PsKey, Appid: 5, }, - Appid: "qun", - Checksum: fmt.Sprintf("%x", md5), - FileLen: size, + Appid: param.UploadType.ReqSessionAppID, + Checksum: fmt.Sprintf("%x", param.CheckSum), + CheckType: param.UploadType.ReqCheckType, + FileLen: param.Size, Env: env{ - Refer: "qzone", + Refer: param.UploadType.ReqRefer, DeviceInfo: "h5", }, - BizReq: bizReq{ - SPicTitle: fileName, - SPicDesc: "", - SAlbumName: albumName, - SAlbumID: albumId, - IBatchID: int(timeStamp), - INeedFeeds: 1, - IUploadTime: int(timeStamp), - MapExt: mapExt{ - Appid: "qun", - Userid: strconv.Itoa(int(groupUin)), - }, - }, - Cmd: "FileUpload", + Model: 0, + Cmd: param.UploadType.ReqSessionCmd, }, }, } + switch param.UploadType.ResourceType { + case ResourceTypePhoto: + reqBody.ControlReq[0].BizReq = imgBizReq{ + commonBizReq: commonBizReq{ + SPicTitle: param.FileName, + SAlbumName: param.AlbumName, + SAlbumID: param.AlbumID, + IBatchID: int(timeStamp), + }, + INeedFeeds: 1, + IUploadTime: int(timeStamp), + MapExt: mapExt{ + Appid: "qun", + Userid: strconv.Itoa(int(param.GroupUin)), + }, + } + case ResourceTypeVideo: + reqBody.ControlReq[0].BizReq = videoBizReq{ + commonBizReq: commonBizReq{ + SPicTitle: param.FileName, + IUploadType: 3, + }, + STitle: param.FileName, + IUploadTime: int(timeStamp), + IPlayTime: 6077.000, // TODO: do we really need a real video length? + IIsNew: 111, + VideoExtInfo: videoExtInfo{ + VideoType: "3", + DomainId: "5", + PhotoNum: "0", + VideoNum: "1", + QunID: strconv.Itoa(int(param.GroupUin)), + }, + } + case ResourceTypeVideoThumbPhoto: + reqBody.ControlReq[0].BizReq = videoThumbImgBizReq{ + imgBizReq: imgBizReq{ + commonBizReq: commonBizReq{ + SPicTitle: param.FileName, + SAlbumName: param.AlbumName, + SAlbumID: param.AlbumID, + IUploadType: 2, + IBatchID: int(param.VidTimeStamp), // parent video upload timestamp + }, + INeedFeeds: 1, + IUploadTime: int(timeStamp), + MapExt: mapExt{ + Appid: param.UploadType.ReqSessionAppID, + Userid: strconv.Itoa(int(param.GroupUin)), + }, + }, + MultiPicInfo: multiPicInfo{ + IBatUploadNum: 1, + }, + STExtendInfo: extendInfo{ + MapParams: mapParams{ + Vid: param.Vid, // parent video vid + PhotoNum: "0", + VideoNum: "1", + }, + }, + STExternalMapExt: externalMapExt{ + IsClientUploadCover: "1", + IsPicVideoMixFeeds: "1", + }, + } + default: + return nil, timeStamp, errors.New("unkown upload type") + } + return reqBody, timeStamp, nil +} + +func (c *QQClient) getGroupAlbumUploadSession(param *uploadSessionParam) (*uploadOptions, int64, error) { + var reqBody interface{} + var err error + var timeStamp int64 = 0 + reqBody, timeStamp, err = c.buildUploadSessionReq(param) + if err != nil { + return nil, timeStamp, err + } reqBodyData, err := json.Marshal(reqBody) if err != nil { - return nil, err + return nil, timeStamp, err } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, - fmt.Sprintf("https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/%x?g_tk=%d", md5, gtk), + fmt.Sprintf("https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/%x?g_tk=%d", param.CheckSum, param.GTK), bytes.NewReader(reqBodyData)) if err != nil { - return nil, err + return nil, timeStamp, err } req.Header.Set("Content-Type", "application/json") resp, err := c.SendRequestWithCookie(req) if err != nil { - return nil, err + return nil, timeStamp, err } respData, err := io.ReadAll(resp.Body) _ = resp.Body.Close() if err != nil { - return nil, err + return nil, timeStamp, err } respJson := gjson.ParseBytes(respData) if respJson.Get("ret").Int() != 0 { - return nil, fmt.Errorf("error: ret:%d, msg:%s", respJson.Get("ret").Int(), respJson.Get("msg").Str) + return nil, timeStamp, fmt.Errorf("error: ret:%d, msg:%s", respJson.Get("ret").Int(), respJson.Get("msg").Str) } return &uploadOptions{ Session: respJson.Get("data.session").Str, BlockSize: int(respJson.Get("data.slice_size").Int()), - }, nil + }, timeStamp, nil } -func (c *QQClient) uploadGroupAlbumBlock(session string, seq, offset, chunkSize, totlaSize, gtk int, chunk []byte, latest bool) (*GroupPhoto, error) { - c.debug("seq:%d,offset:%d,chunksize:%d,totalsize:%d", seq, offset, chunkSize, totlaSize) +func (c *QQClient) uploadGroupAlbumBlock(typ uploadTypeParam, session string, seq, offset, chunkSize, totalSize, gtk int, chunk []byte, latest bool) (rsp *uploadBlockRsp, err error) { + uploadUriCmd := utils.Ternary[string](typ.ResourceType == ResourceTypeVideo, "FileUploadVideo", "FileUpload") body := &bytes.Buffer{} writer := multipart.NewWriter(body) _ = writer.WriteField("uin", strconv.Itoa(int(c.Uin))) - _ = writer.WriteField("appid", "qun") + _ = writer.WriteField("appid", typ.ReqSessionAppID) _ = writer.WriteField("session", session) _ = writer.WriteField("offset", strconv.Itoa(offset)) part, err := writer.CreateFormFile("data", "blob") @@ -156,16 +222,15 @@ func (c *QQClient) uploadGroupAlbumBlock(session string, seq, offset, chunkSize, } _, _ = part.Write(chunk) _ = writer.WriteField("checksum", "") - _ = writer.WriteField("check_type", "0") + _ = writer.WriteField("check_type", strconv.Itoa(typ.ReqCheckType)) _ = writer.WriteField("retry", "0") _ = writer.WriteField("seq", strconv.Itoa(seq)) _ = writer.WriteField("end", strconv.Itoa(offset+chunkSize)) _ = writer.WriteField("cmd", "FileUpload") _ = writer.WriteField("slice_size", strconv.Itoa(chunkSize)) - _ = writer.WriteField("biz_req.iUploadType", "0") _ = writer.Close() - req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://h5.qzone.qq.com/webapp/json/sliceUpload/FileUpload?seq=%d&retry=0&offset=%d&end=%d&total=%d&type=form&g_tk=%d", - seq, offset, offset+chunkSize, totlaSize, gtk), body) + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://h5.qzone.qq.com/webapp/json/sliceUpload/%s?seq=%d&retry=0&offset=%d&end=%d&total=%d&type=form&g_tk=%d", + uploadUriCmd, seq, offset, offset+chunkSize, totalSize, gtk), body) if err != nil { return nil, err } @@ -174,7 +239,7 @@ func (c *QQClient) uploadGroupAlbumBlock(session string, seq, offset, chunkSize, if err != nil { return nil, err } - fmt.Println(resp.StatusCode) + c.debug("uploadGroupAlbumBlock %d | %d | %d | %d | %d", seq, offset, totalSize, chunkSize, resp.StatusCode) respData, err := io.ReadAll(resp.Body) if err != nil { return nil, err @@ -187,37 +252,27 @@ func (c *QQClient) uploadGroupAlbumBlock(session string, seq, offset, chunkSize, if respJson.Get("ret").Int() != 0 { return nil, fmt.Errorf("error: ret:%d, msg:%s", respJson.Get("ret").Int(), respJson.Get("msg").Str) } + if respJson.Get("data.biz.sVid").String() != "" { + c.debug("fetched vid %s", respJson.Get("data.biz.sVid").String()) + return &uploadBlockRsp{ + VID: respJson.Get("data.biz.sVid").Str, + }, nil + } if latest { - return &GroupPhoto{ - ID: respJson.Get("data.biz.sPhotoID").Str, - Url: respJson.Get("data.biz.sBURL").Str, + return &uploadBlockRsp{ + SPhotoID: respJson.Get("data.biz.sPhotoID").Str, + SBURL: respJson.Get("data.biz.sBURL").Str, }, nil } return nil, nil } -func (c *QQClient) UploadGroupAlbum(parms *GroupAlbumUploadParm) (*GroupPhoto, error) { - if parms == nil { - return nil, errors.New("upload parms is nil") - } - defer utils.CloseIO(parms.File) - cookie, err := c.GetCookies("qzone.qq.com") - if err != nil { - return nil, err - } - gtk := GTK(cookie.PsKey) - md5, size := crypto.ComputeMd5AndLength(parms.File) - session, err := c.getGroupAlbunUploadSession(parms.GroupUin, parms.FileName, parms.AlbumId, parms.AlbumName, md5, int(size), gtk) - if err != nil { - return nil, err - } - c.debug("upload group album session %s", session.Session) - offset := 0 - seq := 0 - latest := false - chunk := make([]byte, session.BlockSize) +func (c *QQClient) doUploadGroupAlbumBlock(uos *uploadOptions, usp *uploadSessionParam, file io.ReadSeeker) (rsp *uploadBlockRsp, err error) { + defer utils.CloseIO(file) + offset, seq, latest := 0, 0, false + chunk := make([]byte, uos.BlockSize) for { - chunkSize, err := io.ReadFull(parms.File, chunk) + chunkSize, err := io.ReadFull(file, chunk) if chunkSize == 0 { break } @@ -225,12 +280,12 @@ func (c *QQClient) UploadGroupAlbum(parms *GroupAlbumUploadParm) (*GroupPhoto, e chunk = chunk[:chunkSize] latest = true } - photo, err := c.uploadGroupAlbumBlock(session.Session, seq, offset, chunkSize, int(size), gtk, chunk, latest) + rsp, err = c.uploadGroupAlbumBlock(usp.UploadType, uos.Session, seq, offset, chunkSize, usp.Size, usp.GTK, chunk, latest) if err != nil { return nil, err } if latest { - return photo, err + return rsp, nil } seq += 1 offset += chunkSize @@ -238,6 +293,141 @@ func (c *QQClient) UploadGroupAlbum(parms *GroupAlbumUploadParm) (*GroupPhoto, e return nil, errors.New("upload group album failed: unkown error") } +func (c *QQClient) UploadGroupAlbumPhoto(parms *GroupAlbumUploadParam) (*GroupPhoto, error) { + if parms == nil { + return nil, errors.New("upload parms is nil") + } + cookie, err := c.GetCookies("qzone.qq.com") + if err != nil { + return nil, err + } + gtk := GTK(cookie.PsKey) + md5, size := crypto.ComputeMd5AndLength(parms.Image) + st := uploadTypeParam{ResourceTypePhoto, "qzone", "FileUpload", "qun", 0} + usp := &uploadSessionParam{ + UploadType: st, + GroupUin: parms.GroupUin, + FileName: parms.FileName, + CheckSum: md5, + Size: int(size), + AlbumID: parms.AlbumId, + AlbumName: parms.AlbumName, + GTK: gtk, + } + session, _, err := c.getGroupAlbumUploadSession(usp) + if err != nil { + return nil, err + } + c.debug("upload group album photo start, session %s", session.Session) + ubRsp, err := c.doUploadGroupAlbumBlock(session, usp, parms.Image) + if err != nil { + return nil, err + } + if ubRsp.SPhotoID == "" || ubRsp.SBURL == "" { + return nil, errors.New("upload group album failed because ubRsp missing fields") + } + return &GroupPhoto{ + ID: ubRsp.SPhotoID, + Url: ubRsp.SBURL, + }, nil +} + +func (c *QQClient) UploadGroupAlbumVideo(parms *GroupAlbumUploadParam) (*GroupVideo, error) { + if parms == nil { + return nil, errors.New("upload parms is nil") + } + cookie, err := c.GetCookies("qzone.qq.com") + if err != nil { + return nil, err + } + gtk := GTK(cookie.PsKey) + // upload video + sha1, size := crypto.ComputeSha1AndLength(parms.Video) + st := uploadTypeParam{ResourceTypeVideo, "qzone", "FileUploadVideo", "video_qun", 1} + usp := &uploadSessionParam{ + UploadType: st, + GroupUin: parms.GroupUin, + FileName: parms.FileName, + CheckSum: sha1, + Size: int(size), + AlbumID: parms.AlbumId, + AlbumName: parms.AlbumName, + GTK: gtk, + } + session, timeStamp, err := c.getGroupAlbumUploadSession(usp) + if err != nil { + return nil, err + } + c.debug("upload group album video start, session %s", session.Session) + uvbRsp, err := c.doUploadGroupAlbumBlock(session, usp, parms.Video) + if err != nil { + return nil, err + } + if uvbRsp.VID == "" { + return nil, errors.New("upload failed because the vid is missing in the upload group video response") + } + // upload video thumbnail + md5, size := crypto.ComputeMd5AndLength(parms.Thumbnail) + st = uploadTypeParam{ResourceTypeVideoThumbPhoto, "huodong", "", "qun", 0} + usp = &uploadSessionParam{ + UploadType: st, + GroupUin: parms.GroupUin, + FileName: parms.FileName, + CheckSum: md5, + Size: int(size), + AlbumID: parms.AlbumId, + AlbumName: parms.AlbumName, + VidTimeStamp: timeStamp, + Vid: uvbRsp.VID, + GTK: gtk, + } + session, _, err = c.getGroupAlbumUploadSession(usp) + if err != nil { + return nil, err + } + c.debug("upload group album video thumb start, session %s", session.Session) + utbRsp, err := c.doUploadGroupAlbumBlock(session, usp, parms.Thumbnail) + if err != nil { + return nil, err + } + if utbRsp.SPhotoID == "" || utbRsp.SBURL == "" { + return nil, errors.New("upload group album failed because utbRsp missing fields") + } + return &GroupVideo{}, nil // TODO: where to get the video url? +} + +type ResourceType int + +const ( + ResourceTypeUnknown ResourceType = iota + ResourceTypePhoto + ResourceTypeVideoThumbPhoto + ResourceTypeVideo +) + +type ( + uploadSessionParam struct { + UploadType uploadTypeParam + GroupUin uint32 + FileName string + CheckSum []byte + Size int + AlbumID string + AlbumName string + VidTimeStamp int64 + Vid string + GTK int + } + + uploadTypeParam struct { + ResourceType + ReqRefer string + ReqSessionCmd string + ReqSessionAppID string + ReqCheckType int + } +) + type ( GroupAlbum struct { Name string @@ -254,10 +444,26 @@ type ( Url string } - GroupAlbumUploadParm struct { + GroupVideo struct { + ID string + Url string + } + + ImageFile struct { + Image io.ReadSeeker + } + + VideoFile struct { + Thumbnail io.ReadSeeker + Video io.ReadSeeker + } + + GroupAlbumUploadParam struct { + ResourceType GroupUin uint32 FileName, AlbumId, AlbumName string - File io.ReadSeeker + ImageFile + VideoFile } uploadOptions struct { @@ -266,23 +472,23 @@ type ( } // request upload session req - groupAlbunUploadReq struct { + groupAlbumUploadReq struct { ControlReq []controlReq `json:"control_req"` } controlReq struct { - Uin string `json:"uin"` - Token token `json:"token"` - Appid string `json:"appid"` - Checksum string `json:"checksum"` - CheckType int `json:"check_type"` - FileLen int `json:"file_len"` - Env env `json:"env"` - Model int `json:"model"` - BizReq bizReq `json:"biz_req"` - Session string `json:"session"` - AsyUpload int `json:"asy_upload"` - Cmd string `json:"cmd"` + Uin string `json:"uin"` + Token token `json:"token"` + Appid string `json:"appid"` + Checksum string `json:"checksum"` + CheckType int `json:"check_type"` + FileLen int `json:"file_len"` + Env env `json:"env"` + Model int `json:"model"` + BizReq interface{} `json:"biz_req"` + Session string `json:"session"` + AsyUpload int `json:"asy_upload"` + Cmd string `json:"cmd"` } token struct { @@ -296,7 +502,7 @@ type ( DeviceInfo string `json:"deviceInfo"` } - bizReq struct { + commonBizReq struct { SPicTitle string `json:"sPicTitle"` SPicDesc string `json:"sPicDesc"` SAlbumName string `json:"sAlbumName"` @@ -311,13 +517,81 @@ type ( IPicHight int `json:"iPicHight"` IWaterType int `json:"iWaterType"` IDistinctUse int `json:"iDistinctUse"` - INeedFeeds int `json:"iNeedFeeds"` - IUploadTime int `json:"iUploadTime"` - MapExt mapExt `json:"mapExt"` + } + + imgBizReq struct { + commonBizReq + INeedFeeds int `json:"iNeedFeeds"` + IUploadTime int `json:"iUploadTime"` + MapExt mapExt `json:"mapExt"` + } + + videoBizReq struct { + commonBizReq + STitle string `json:"sTitle"` + SDesc string `json:"sDesc"` + IFlag int `json:"iFlag"` + IUploadTime int `json:"iUploadTime"` + IPlayTime float64 `json:"iPlayTime"` + SCoverUrl string `json:"sCoverUrl"` + IIsNew int `json:"iIsNew"` + IIsOriginalVideo int `json:"iIsOriginalVideo"` + IIsFormatF20 int `json:"iIsFormatF20"` + VideoExtInfo videoExtInfo `json:"extend_info"` + } + + videoThumbImgBizReq struct { + imgBizReq + MultiPicInfo multiPicInfo `json:"mutliPicInfo"` // 没错,tx拼错了 + STExtendInfo extendInfo `json:"stExtendInfo"` + STExternalMapExt externalMapExt `json:"stExternalMapExt"` + CameraMaker string `json:"sExif_CameraMaker"` + CameraModel string `json:"sExif_CameraModel"` + Time string `json:"sExif_Time"` + LatitudeRef string `json:"sExif_LatitudeRef"` + Latitude string `json:"sExif_Latitude"` + LongitudeRef string `json:"sExif_LongitudeRef"` + Longitude string `json:"sExif_Longitude"` } mapExt struct { Appid string `json:"appid"` Userid string `json:"userid"` } + + videoExtInfo struct { + VideoType string `json:"video_type"` + DomainId string `json:"domainid"` + PhotoNum string `json:"photo_num"` + VideoNum string `json:"video_num"` + QunID string `json:"qun_id"` + } + + multiPicInfo struct { + IBatUploadNum int `json:"iBatUploadNum"` + ICurUpload int `json:"iCurUpload"` + ISuccNum int `json:"iSuccNum"` + IFailNum int `json:"iFailNum"` + } + + extendInfo struct { + MapParams mapParams `json:"mapParams"` + } + + mapParams struct { + Vid string `json:"vid"` + PhotoNum string `json:"photo_num"` + VideoNum string `json:"video_num"` + } + + externalMapExt struct { + IsClientUploadCover string `json:"is_client_upload_cover"` + IsPicVideoMixFeeds string `json:"is_pic_video_mix_feeds"` + } + + uploadBlockRsp struct { + SPhotoID string + SBURL string + VID string + } ) From b36a010d5a16608319ddeb2bac514342242990b8 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Fri, 30 Aug 2024 01:30:28 +0800 Subject: [PATCH 2/3] chore: resolve `golangci-lint` scan's issues --- client/album.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/album.go b/client/album.go index ceffbe5..2b5b33c 100644 --- a/client/album.go +++ b/client/album.go @@ -169,10 +169,7 @@ func (c *QQClient) buildUploadSessionReq(param *uploadSessionParam) (*groupAlbum } func (c *QQClient) getGroupAlbumUploadSession(param *uploadSessionParam) (*uploadOptions, int64, error) { - var reqBody interface{} - var err error - var timeStamp int64 = 0 - reqBody, timeStamp, err = c.buildUploadSessionReq(param) + reqBody, timeStamp, err := c.buildUploadSessionReq(param) if err != nil { return nil, timeStamp, err } From 7e9b76a90fda0780e5b2a213e86b30d0907c9ee1 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Fri, 30 Aug 2024 19:54:34 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=8B=89?= =?UTF-8?q?=E5=8F=96=E7=BE=A4=E7=9B=B8=E5=86=8C=E8=AF=A6=E6=83=85=E5=86=85?= =?UTF-8?q?=E5=AE=B9=EF=BC=8C=E5=88=9D=E6=AD=A5=E8=A7=A3=E5=86=B3=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E8=A7=86=E9=A2=91=E5=90=8E=E6=97=A0=E6=B3=95=E5=8F=96?= =?UTF-8?q?=E5=BE=97URL=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/album.go | 99 ++++++++++++- client/packets/album/get_media_list.go | 41 ++++++ .../service/album/QunMedia_GetMediaList.pb.go | 131 ++++++++++++++++++ .../service/album/QunMedia_GetMediaList.proto | 116 ++++++++++++++++ utils/time.go | 13 ++ 5 files changed, 394 insertions(+), 6 deletions(-) create mode 100644 client/packets/album/get_media_list.go create mode 100644 client/packets/pb/service/album/QunMedia_GetMediaList.pb.go create mode 100644 client/packets/pb/service/album/QunMedia_GetMediaList.proto create mode 100644 utils/time.go diff --git a/client/album.go b/client/album.go index 2b5b33c..15a72fe 100644 --- a/client/album.go +++ b/client/album.go @@ -6,14 +6,16 @@ import ( "encoding/json" "errors" "fmt" - "github.com/LagrangeDev/LagrangeGo/utils" - "github.com/LagrangeDev/LagrangeGo/utils/crypto" - "github.com/tidwall/gjson" "io" "mime/multipart" "net/http" "strconv" "time" + + "github.com/LagrangeDev/LagrangeGo/client/packets/album" + "github.com/LagrangeDev/LagrangeGo/utils" + "github.com/LagrangeDev/LagrangeGo/utils/crypto" + "github.com/tidwall/gjson" ) const TimeLayout = "2006-01-02 15:04:05" @@ -56,6 +58,7 @@ func (c *QQClient) GetGroupAlbum(groupUin uint32) ([]*GroupAlbum, error) { fmt.Println(err) } grpAlbumList[i] = &GroupAlbum{ + GroupUin: groupUin, Name: v.Get("title").Str, ID: v.Get("id").Str, Description: v.Get("desc").Str, @@ -68,6 +71,59 @@ func (c *QQClient) GetGroupAlbum(groupUin uint32) ([]*GroupAlbum, error) { return grpAlbumList, nil } +func (c *QQClient) GetGroupAlbumElem(ab *GroupAlbum) ([]*GroupAlbumElem, error) { + var elem []*GroupAlbumElem + pageInfo := "" + for { + pkt, err := album.BuildGetMediaListReq(c.Uin, ab.GroupUin, ab.ID, pageInfo) + if err != nil { + return nil, err + } + payload, err := c.sendUniPacketAndWait("QunAlbum.trpc.qzone.webapp_qun_media.QunMedia.GetMediaList", pkt) + if err != nil { + return nil, err + } + resp, err := album.ParseGetMediaListResp(payload) + if err != nil { + return nil, err + } + for _, v := range resp.Body.ElemInfo { + if v.ImgInfo != nil { + elem = append(elem, &GroupAlbumElem{ + photo: &GroupPhoto{ + ID: v.ImgInfo.ImageID, + Url: v.ImgInfo.ImgLinkInfo.ImageURL, + }, + operatorUserInfo: &GroupAlbumElemUserInfo{ + UserNickName: v.UploaderInfo.UserNickName, + UserUin: v.UploaderInfo.UserUin, + }, + }) + } + if v.VideoInfo != nil { + elem = append(elem, &GroupAlbumElem{ + video: &GroupVideo{ + ID: v.VideoInfo.VideoID, + Url: v.VideoInfo.VideoURL, + }, + operatorUserInfo: &GroupAlbumElemUserInfo{ + UserNickName: v.UploaderInfo.UserNickName, + UserUin: v.UploaderInfo.UserUin, + }, + }) + } + } + if resp.Body.PageInfo.IsNone() || gjson.Get(resp.Body.PageInfo.Unwrap(), "Loc.return_num").Int() == 0 { + break + } + if resp.Body.ElemInfo == nil && resp.Body.ElemMetaInfo == nil { + break + } + pageInfo = resp.Body.PageInfo.Unwrap() + } + return elem, nil +} + func (c *QQClient) buildUploadSessionReq(param *uploadSessionParam) (*groupAlbumUploadReq, int64, error) { timeStamp := time.Now().Unix() cookies, err := c.GetCookies("qzone.qq.com") @@ -226,7 +282,9 @@ func (c *QQClient) uploadGroupAlbumBlock(typ uploadTypeParam, session string, se _ = writer.WriteField("cmd", "FileUpload") _ = writer.WriteField("slice_size", strconv.Itoa(chunkSize)) _ = writer.Close() - req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://h5.qzone.qq.com/webapp/json/sliceUpload/%s?seq=%d&retry=0&offset=%d&end=%d&total=%d&type=form&g_tk=%d", + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://h5.qzone.qq.com/webapp/json/sliceUpload/%s?seq=%d&retry=0&offset=%d&end=%d&total=%d&type=form&g_tk=%d", uploadUriCmd, seq, offset, offset+chunkSize, totalSize, gtk), body) if err != nil { return nil, err @@ -388,9 +446,26 @@ func (c *QQClient) UploadGroupAlbumVideo(parms *GroupAlbumUploadParam) (*GroupVi return nil, err } if utbRsp.SPhotoID == "" || utbRsp.SBURL == "" { - return nil, errors.New("upload group album failed because utbRsp missing fields") + return nil, errors.New("upload group album failed because missing video thumbnail fields") } - return &GroupVideo{}, nil // TODO: where to get the video url? + // get video url + updList, err := c.GetGroupAlbumElem(&GroupAlbum{ + GroupUin: parms.GroupUin, + ID: parms.AlbumId, + }) + if err != nil { + return nil, err + } + // simple loop, enough + for _, elem := range updList { + if elem.video != nil && elem.video.ID == uvbRsp.VID { + return &GroupVideo{ + ID: elem.video.ID, + Url: elem.video.Url, + }, nil + } + } + return &GroupVideo{}, errors.New("upload success but cannot get video url") } type ResourceType int @@ -427,6 +502,7 @@ type ( type ( GroupAlbum struct { + GroupUin uint32 Name string ID string Description string @@ -436,6 +512,12 @@ type ( CreateTime int64 } + GroupAlbumElem struct { + photo *GroupPhoto + video *GroupVideo + operatorUserInfo *GroupAlbumElemUserInfo + } + GroupPhoto struct { ID string Url string @@ -446,6 +528,11 @@ type ( Url string } + GroupAlbumElemUserInfo struct { + UserNickName string + UserUin string + } + ImageFile struct { Image io.ReadSeeker } diff --git a/client/packets/album/get_media_list.go b/client/packets/album/get_media_list.go new file mode 100644 index 0000000..521fd7d --- /dev/null +++ b/client/packets/album/get_media_list.go @@ -0,0 +1,41 @@ +package album + +import ( + "errors" + "strconv" + + "github.com/LagrangeDev/LagrangeGo/client/packets/pb/service/album" + "github.com/LagrangeDev/LagrangeGo/internal/proto" + "github.com/LagrangeDev/LagrangeGo/utils" +) + +func BuildGetMediaListReq(selfUin uint32, groupUin uint32, albumId string, pageInfo string) ([]byte, error) { + return proto.Marshal(&album.QzoneGetMediaList{ + Field1: 0, + Field2: "h5_test", + Field3: "h5_test", + Field4: &album.QzoneGetMediaList_F4{ + GroupID: strconv.Itoa(int(groupUin)), + AlbumID: albumId, + Field3: 0, + Field4: "", + PageInfo: pageInfo, + }, + UinTimeStamp: utils.GenerateUinTimestamp(selfUin), + Field10: &album.QzoneGetMediaList_F10{ + AppIdFlag: "fc-appid", + AppIdValue: "100", + }, + }) +} + +func ParseGetMediaListResp(data []byte) (resp *album.QzoneGetMediaList_Response, err error) { + resp = &album.QzoneGetMediaList_Response{} + if err = proto.Unmarshal(data, resp); err != nil { + return nil, err + } + if resp.ErrorCode.IsSome() && resp.ErrorMsg.IsSome() { + return nil, errors.New(resp.ErrorMsg.Unwrap()) + } + return resp, nil +} diff --git a/client/packets/pb/service/album/QunMedia_GetMediaList.pb.go b/client/packets/pb/service/album/QunMedia_GetMediaList.pb.go new file mode 100644 index 0000000..61a442b --- /dev/null +++ b/client/packets/pb/service/album/QunMedia_GetMediaList.pb.go @@ -0,0 +1,131 @@ +// Code generated by protoc-gen-golite. DO NOT EDIT. +// source: pb/service/album/QunMedia_GetMediaList.proto + +package album + +import ( + proto "github.com/RomiChan/protobuf/proto" +) + +type QzoneGetMediaList struct { + Field1 uint32 `protobuf:"varint,1,opt"` + Field2 string `protobuf:"bytes,2,opt"` + Field3 string `protobuf:"bytes,3,opt"` + Field4 *QzoneGetMediaList_F4 `protobuf:"bytes,4,opt"` + UinTimeStamp string `protobuf:"bytes,5,opt"` + Field10 *QzoneGetMediaList_F10 `protobuf:"bytes,10,opt"` + _ [0]func() +} + +type QzoneGetMediaList_F4 struct { + GroupID string `protobuf:"bytes,1,opt"` + AlbumID string `protobuf:"bytes,2,opt"` + Field3 uint32 `protobuf:"varint,3,opt"` + Field4 string `protobuf:"bytes,4,opt"` + PageInfo string `protobuf:"bytes,5,opt"` + _ [0]func() +} + +type QzoneGetMediaList_F10 struct { + AppIdFlag string `protobuf:"bytes,1,opt"` + AppIdValue string `protobuf:"bytes,2,opt"` + _ [0]func() +} + +type QzoneGetMediaList_Response struct { + Field1 proto.Option[uint32] `protobuf:"varint,1,opt"` + ErrorCode proto.Option[uint32] `protobuf:"varint,2,opt"` + ErrorMsg proto.Option[string] `protobuf:"bytes,3,opt"` + Body *QzoneGetMediaList_Response_Body `protobuf:"bytes,4,opt"` + _ [0]func() +} + +type QzoneGetMediaList_Response_Body struct { + AlbumInfo *AlbumInfo `protobuf:"bytes,1,opt"` + ElemMetaInfo []*AlbumElemMetaInfo `protobuf:"bytes,2,rep"` + ElemInfo []*AlbumElemInfo `protobuf:"bytes,3,rep"` + PageInfo proto.Option[string] `protobuf:"bytes,5,opt"` +} + +type AlbumInfo struct { + AlbumID string `protobuf:"bytes,1,opt"` + AlbumName string `protobuf:"bytes,3,opt"` + CreateTime uint32 `protobuf:"varint,5,opt"` + LastModifyTime uint32 `protobuf:"varint,6,opt"` + LastUploadTime uint32 `protobuf:"varint,7,opt"` + Count uint32 `protobuf:"varint,8,opt"` + ThumbInfo *AlbumThumbInfo `protobuf:"bytes,9,opt"` + CreatorInfo *UserInfo `protobuf:"bytes,10,opt"` + _ [0]func() +} + +type AlbumThumbInfo struct { + ThumbImageInfo *AlbumThumbImageInfo `protobuf:"bytes,2,opt"` + _ [0]func() +} + +type AlbumThumbImageInfo struct { + ImageID string `protobuf:"bytes,3,opt"` + ImgLinkInfos []*AlbumElemImgLinkComplexInfo `protobuf:"bytes,4,rep"` + ImgLinkInfo *AlbumElemImgLinkInfo `protobuf:"bytes,5,opt"` +} + +type AlbumElemMetaInfo struct { + UploadTimeStamp uint32 `protobuf:"varint,1,opt"` + UploadDate string `protobuf:"bytes,3,opt"` + UploaderInfo *UserInfo `protobuf:"bytes,4,opt"` + _ [0]func() +} + +type AlbumElemInfo struct { + ImgInfo *AlbumElemImgInfo `protobuf:"bytes,2,opt"` + VideoInfo *AlbumElemVideoInfo `protobuf:"bytes,3,opt"` + UploaderUin string `protobuf:"bytes,4,opt"` + LastModifyTime uint32 `protobuf:"varint,7,opt"` + LastUploadTime uint32 `protobuf:"varint,8,opt"` + UploaderInfo *UserInfo `protobuf:"bytes,12,opt"` + _ [0]func() +} + +type AlbumElemImgInfo struct { + FileName string `protobuf:"bytes,1,opt"` + ImageID string `protobuf:"bytes,3,opt"` + ImgLinkInfos []*AlbumElemImgLinkComplexInfo `protobuf:"bytes,4,rep"` + ImgLinkInfo *AlbumElemImgLinkInfo `protobuf:"bytes,5,opt"` +} + +type AlbumElemImgLinkComplexInfo struct { + Seq uint32 `protobuf:"varint,1,opt"` + ImgLinkInfo *AlbumElemImgLinkInfo `protobuf:"bytes,2,opt"` + _ [0]func() +} + +type AlbumElemImgLinkInfo struct { + ImageURL string `protobuf:"bytes,1,opt"` + Height uint32 `protobuf:"varint,2,opt"` + Width uint32 `protobuf:"varint,3,opt"` + _ [0]func() +} + +type AlbumElemVideoInfo struct { + VideoID string `protobuf:"bytes,1,opt"` + VideoURL string `protobuf:"bytes,2,opt"` + VideoMetaInfo *AlbumElemVideoMetaInfo `protobuf:"bytes,3,opt"` + VideoHeight uint32 `protobuf:"varint,4,opt"` + VideoWidth uint32 `protobuf:"varint,5,opt"` + VideoThumbImageInfo *AlbumElemImgLinkComplexInfo `protobuf:"bytes,7,opt"` + _ [0]func() +} + +type AlbumElemVideoMetaInfo struct { + VideoName string `protobuf:"bytes,1,opt"` + VideoThumbImageID string `protobuf:"bytes,3,opt"` + VideoThumbImageInfos []*AlbumElemImgLinkComplexInfo `protobuf:"bytes,4,rep"` + VideoThumbImageInfo *AlbumElemImgLinkInfo `protobuf:"bytes,5,opt"` +} + +type UserInfo struct { + UserNickName string `protobuf:"bytes,2,opt"` + UserUin string `protobuf:"bytes,13,opt"` + _ [0]func() +} diff --git a/client/packets/pb/service/album/QunMedia_GetMediaList.proto b/client/packets/pb/service/album/QunMedia_GetMediaList.proto new file mode 100644 index 0000000..adb3e77 --- /dev/null +++ b/client/packets/pb/service/album/QunMedia_GetMediaList.proto @@ -0,0 +1,116 @@ +syntax = "proto3"; + +option go_package = "github.com/LagrangeDev/LagrangeGo/client/packets/pb/service/album"; + +message QzoneGetMediaList { + uint32 Field1 = 1; + string Field2 = 2; + string Field3 = 3; + QzoneGetMediaList_F4 Field4 = 4; + string UinTimeStamp = 5; + QzoneGetMediaList_F10 Field10 = 10; +} + +message QzoneGetMediaList_F4 { + string GroupID = 1; + string AlbumID = 2; + uint32 Field3 = 3; + string Field4 = 4; + string PageInfo = 5; +} + +message QzoneGetMediaList_F10 { + string AppIdFlag = 1; + string AppIdValue = 2; +} + +message QzoneGetMediaList_Response{ + optional uint32 Field1 = 1; + optional uint32 ErrorCode = 2; + optional string ErrorMsg = 3; + optional QzoneGetMediaList_Response_Body Body = 4; +} + +message QzoneGetMediaList_Response_Body { + AlbumInfo AlbumInfo = 1; + repeated AlbumElemMetaInfo ElemMetaInfo = 2; + repeated AlbumElemInfo ElemInfo = 3; + optional string PageInfo = 5; +} + +message AlbumInfo { + string AlbumID = 1; + string AlbumName = 3; + uint32 CreateTime = 5; + uint32 LastModifyTime = 6; + uint32 LastUploadTime = 7; + uint32 Count = 8; + AlbumThumbInfo ThumbInfo = 9; + UserInfo CreatorInfo = 10; +} + +message AlbumThumbInfo { + AlbumThumbImageInfo ThumbImageInfo = 2; +} + +message AlbumThumbImageInfo { + string ImageID = 3; + repeated AlbumElemImgLinkComplexInfo ImgLinkInfos = 4; + AlbumElemImgLinkInfo ImgLinkInfo = 5; +} + +message AlbumElemMetaInfo { + uint32 UploadTimeStamp = 1; + string UploadDate = 3; + UserInfo UploaderInfo = 4; +} + +message AlbumElemInfo { + optional AlbumElemImgInfo ImgInfo = 2; + optional AlbumElemVideoInfo VideoInfo = 3; + string UploaderUin = 4; + uint32 LastModifyTime = 7; + uint32 LastUploadTime = 8; + UserInfo UploaderInfo = 12; +} + +message AlbumElemImgInfo { + string FileName = 1; + string ImageID = 3; + repeated AlbumElemImgLinkComplexInfo ImgLinkInfos = 4; + AlbumElemImgLinkInfo ImgLinkInfo = 5; +} + +message AlbumElemImgLinkComplexInfo { + uint32 Seq = 1; + AlbumElemImgLinkInfo ImgLinkInfo = 2; +} + +message AlbumElemImgLinkInfo { + string ImageURL = 1; + uint32 Height = 2; + uint32 Width = 3; +} + +message AlbumElemVideoInfo { + string VideoID = 1; + string VideoURL = 2; + AlbumElemVideoMetaInfo VideoMetaInfo = 3; + uint32 VideoHeight = 4; + uint32 VideoWidth = 5; + AlbumElemImgLinkComplexInfo VideoThumbImageInfo = 7; +} + +message AlbumElemVideoMetaInfo { + string VideoName = 1; + string VideoThumbImageID = 3; + repeated AlbumElemImgLinkComplexInfo VideoThumbImageInfos = 4; + AlbumElemImgLinkInfo VideoThumbImageInfo = 5; +} + +message UserInfo { + string UserNickName = 2; + string UserUin = 13; +} + + diff --git a/utils/time.go b/utils/time.go new file mode 100644 index 0000000..156f9c6 --- /dev/null +++ b/utils/time.go @@ -0,0 +1,13 @@ +package utils + +import ( + "fmt" + "time" +) + +func GenerateUinTimestamp(uin uint32) string { + currentTime := time.Now() + formattedTime := currentTime.Format("0102150405") + milliseconds := currentTime.Nanosecond() / 1000000 + return fmt.Sprintf("%d_%s%02d_%d", uin, formattedTime, currentTime.Year()%100, milliseconds) +}