package _139 import ( "encoding/base64" "errors" "fmt" "net/http" "net/url" "sort" "strconv" "strings" "time" "github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils/random" "github.com/alist-org/alist/v3/internal/op" "github.com/go-resty/resty/v2" jsoniter "github.com/json-iterator/go" log "github.com/sirupsen/logrus" ) // do others that not defined in Driver interface func (d *Yun139) isFamily() bool { return d.Type == "family" } func encodeURIComponent(str string) string { r := url.QueryEscape(str) r = strings.Replace(r, "+", "%20", -1) r = strings.Replace(r, "%21", "!", -1) r = strings.Replace(r, "%27", "'", -1) r = strings.Replace(r, "%28", "(", -1) r = strings.Replace(r, "%29", ")", -1) r = strings.Replace(r, "%2A", "*", -1) return r } func calSign(body, ts, randStr string) string { body = encodeURIComponent(body) strs := strings.Split(body, "") sort.Strings(strs) body = strings.Join(strs, "") body = base64.StdEncoding.EncodeToString([]byte(body)) res := utils.GetMD5EncodeStr(body) + utils.GetMD5EncodeStr(ts+":"+randStr) res = strings.ToUpper(utils.GetMD5EncodeStr(res)) return res } func getTime(t string) time.Time { stamp, _ := time.ParseInLocation("20060102150405", t, utils.CNLoc) return stamp } func (d *Yun139) refreshToken() error { url := "https://aas.caiyun.feixin.10086.cn:443/tellin/authTokenRefresh.do" var resp RefreshTokenResp decode, err := base64.StdEncoding.DecodeString(d.Authorization) if err != nil { return err } decodeStr := string(decode) splits := strings.Split(decodeStr, ":") reqBody := "" + splits[2] + "" + splits[1] + "656" _, err = base.RestyClient.R(). ForceContentType("application/xml"). SetBody(reqBody). SetResult(&resp). Post(url) if err != nil { return err } if resp.Return != "0" { return fmt.Errorf("failed to refresh token: %s", resp.Desc) } d.Authorization = base64.StdEncoding.EncodeToString([]byte(splits[0] + ":" + splits[1] + ":" + resp.Token)) op.MustSaveDriverStorage(d) return nil } func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { url := "https://yun.139.com" + pathname req := base.RestyClient.R() randStr := random.String(16) ts := time.Now().Format("2006-01-02 15:04:05") if callback != nil { callback(req) } body, err := utils.Json.Marshal(req.Body) if err != nil { return nil, err } sign := calSign(string(body), ts, randStr) svcType := "1" if d.isFamily() { svcType = "2" } req.SetHeaders(map[string]string{ "Accept": "application/json, text/plain, */*", "CMS-DEVICE": "default", "Authorization": "Basic " + d.Authorization, "mcloud-channel": "1000101", "mcloud-client": "10701", //"mcloud-route": "001", "mcloud-sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign), //"mcloud-skey":"", "mcloud-version": "6.6.0", "Origin": "https://yun.139.com", "Referer": "https://yun.139.com/w/", "x-DeviceInfo": "||9|6.6.0|chrome|95.0.4638.69|uwIy75obnsRPIwlJSd7D9GhUvFwG96ce||macos 10.15.2||zh-CN|||", "x-huawei-channelSrc": "10000034", "x-inner-ntwk": "2", "x-m4c-caller": "PC", "x-m4c-src": "10002", "x-SvcType": svcType, }) var e BaseResp req.SetResult(&e) res, err := req.Execute(method, url) log.Debugln(res.String()) if !e.Success { return nil, errors.New(e.Message) } if resp != nil { err = utils.Json.Unmarshal(res.Body(), resp) if err != nil { return nil, err } } return res.Body(), nil } func (d *Yun139) post(pathname string, data interface{}, resp interface{}) ([]byte, error) { return d.request(pathname, http.MethodPost, func(req *resty.Request) { req.SetBody(data) }, resp) } func (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) { start := 0 limit := 100 files := make([]model.Obj, 0) for { data := base.Json{ "catalogID": catalogID, "sortDirection": 1, "startNumber": start + 1, "endNumber": start + limit, "filterType": 0, "catalogSortType": 0, "contentSortType": 0, "commonAccountInfo": base.Json{ "account": d.Account, "accountType": 1, }, } var resp GetDiskResp _, err := d.post("/orchestration/personalCloud/catalog/v1.0/getDisk", data, &resp) if err != nil { return nil, err } for _, catalog := range resp.Data.GetDiskResult.CatalogList { f := model.Object{ ID: catalog.CatalogID, Name: catalog.CatalogName, Size: 0, Modified: getTime(catalog.UpdateTime), Ctime: getTime(catalog.CreateTime), IsFolder: true, } files = append(files, &f) } for _, content := range resp.Data.GetDiskResult.ContentList { f := model.ObjThumb{ Object: model.Object{ ID: content.ContentID, Name: content.ContentName, Size: content.ContentSize, Modified: getTime(content.UpdateTime), HashInfo: utils.NewHashInfo(utils.MD5, content.Digest), }, Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL}, //Thumbnail: content.BigthumbnailURL, } files = append(files, &f) } if start+limit >= resp.Data.GetDiskResult.NodeCount { break } start += limit } return files, nil } func (d *Yun139) newJson(data map[string]interface{}) base.Json { common := map[string]interface{}{ "catalogType": 3, "cloudID": d.CloudID, "cloudType": 1, "commonAccountInfo": base.Json{ "account": d.Account, "accountType": 1, }, } return utils.MergeMap(data, common) } func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) { pageNum := 1 files := make([]model.Obj, 0) for { data := d.newJson(base.Json{ "catalogID": catalogID, "contentSortType": 0, "pageInfo": base.Json{ "pageNum": pageNum, "pageSize": 100, }, "sortDirection": 1, }) var resp QueryContentListResp _, err := d.post("/orchestration/familyCloud/content/v1.0/queryContentList", data, &resp) if err != nil { return nil, err } for _, catalog := range resp.Data.CloudCatalogList { f := model.Object{ ID: catalog.CatalogID, Name: catalog.CatalogName, Size: 0, IsFolder: true, Modified: getTime(catalog.LastUpdateTime), Ctime: getTime(catalog.CreateTime), } files = append(files, &f) } for _, content := range resp.Data.CloudContentList { f := model.ObjThumb{ Object: model.Object{ ID: content.ContentID, Name: content.ContentName, Size: content.ContentSize, Modified: getTime(content.LastUpdateTime), Ctime: getTime(content.CreateTime), }, Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL}, //Thumbnail: content.BigthumbnailURL, } files = append(files, &f) } if 100*pageNum > resp.Data.TotalCount { break } pageNum++ } return files, nil } func (d *Yun139) getLink(contentId string) (string, error) { data := base.Json{ "appName": "", "contentID": contentId, "commonAccountInfo": base.Json{ "account": d.Account, "accountType": 1, }, } res, err := d.post("/orchestration/personalCloud/uploadAndDownload/v1.0/downloadRequest", data, nil) if err != nil { return "", err } return jsoniter.Get(res, "data", "downloadURL").ToString(), nil } func unicode(str string) string { textQuoted := strconv.QuoteToASCII(str) textUnquoted := textQuoted[1 : len(textQuoted)-1] return textUnquoted } func (d *Yun139) personalRequest(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { url := "https://personal-kd-njs.yun.139.com" + pathname req := base.RestyClient.R() randStr := random.String(16) ts := time.Now().Format("2006-01-02 15:04:05") if callback != nil { callback(req) } body, err := utils.Json.Marshal(req.Body) if err != nil { return nil, err } sign := calSign(string(body), ts, randStr) svcType := "1" if d.isFamily() { svcType = "2" } req.SetHeaders(map[string]string{ "Accept": "application/json, text/plain, */*", "Authorization": "Basic " + d.Authorization, "Caller": "web", "Cms-Device": "default", "Mcloud-Channel": "1000101", "Mcloud-Client": "10701", "Mcloud-Route": "001", "Mcloud-Sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign), "Mcloud-Version": "7.13.0", "Origin": "https://yun.139.com", "Referer": "https://yun.139.com/w/", "x-DeviceInfo": "||9|7.13.0|chrome|120.0.0.0|||windows 10||zh-CN|||", "x-huawei-channelSrc": "10000034", "x-inner-ntwk": "2", "x-m4c-caller": "PC", "x-m4c-src": "10002", "x-SvcType": svcType, "X-Yun-Api-Version": "v1", "X-Yun-App-Channel": "10000034", "X-Yun-Channel-Source": "10000034", "X-Yun-Client-Info": "||9|7.13.0|chrome|120.0.0.0|||windows 10||zh-CN|||dW5kZWZpbmVk||", "X-Yun-Module-Type": "100", "X-Yun-Svc-Type": "1", }) var e BaseResp req.SetResult(&e) res, err := req.Execute(method, url) if err != nil { return nil, err } log.Debugln(res.String()) if !e.Success { return nil, errors.New(e.Message) } if resp != nil { err = utils.Json.Unmarshal(res.Body(), resp) if err != nil { return nil, err } } return res.Body(), nil } func (d *Yun139) personalPost(pathname string, data interface{}, resp interface{}) ([]byte, error) { return d.personalRequest(pathname, http.MethodPost, func(req *resty.Request) { req.SetBody(data) }, resp) } func getPersonalTime(t string) time.Time { stamp, err := time.ParseInLocation("2006-01-02T15:04:05.999-07:00", t, utils.CNLoc) if err != nil { panic(err) } return stamp } func (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) { files := make([]model.Obj, 0) nextPageCursor := "" for { data := base.Json{ "imageThumbnailStyleList": []string{"Small", "Large"}, "orderBy": "updated_at", "orderDirection": "DESC", "pageInfo": base.Json{ "pageCursor": nextPageCursor, "pageSize": 100, }, "parentFileId": fileId, } var resp PersonalListResp _, err := d.personalPost("/hcy/file/list", data, &resp) if err != nil { return nil, err } nextPageCursor = resp.Data.NextPageCursor for _, item := range resp.Data.Items { var isFolder = (item.Type == "folder") var f model.Obj if isFolder { f = &model.Object{ ID: item.FileId, Name: item.Name, Size: 0, Modified: getPersonalTime(item.UpdatedAt), Ctime: getPersonalTime(item.CreatedAt), IsFolder: isFolder, } } else { var Thumbnails = item.Thumbnails var ThumbnailUrl string if len(Thumbnails) > 0 { ThumbnailUrl = Thumbnails[len(Thumbnails)-1].Url } f = &model.ObjThumb{ Object: model.Object{ ID: item.FileId, Name: item.Name, Size: item.Size, Modified: getPersonalTime(item.UpdatedAt), Ctime: getPersonalTime(item.CreatedAt), IsFolder: isFolder, }, Thumbnail: model.Thumbnail{Thumbnail: ThumbnailUrl}, } } files = append(files, f) } if len(nextPageCursor) == 0 { break } } return files, nil } func (d *Yun139) personalGetLink(fileId string) (string, error) { data := base.Json{ "fileId": fileId, } res, err := d.personalPost("/hcy/file/getDownloadUrl", data, nil) if err != nil { return "", err } var cdnUrl = jsoniter.Get(res, "data", "cdnUrl").ToString() if cdnUrl != "" { return cdnUrl, nil } else { return jsoniter.Get(res, "data", "url").ToString(), nil } }