Repository: AlistGo/alist (language: go)

Issue:
能添加挂载移动云盘共享群群组文件的功能吗
### Please make sure of the following things

- [ ] I have read the [documentation](https://alist.nn.ci).
- [ ] I'm sure there are no duplicate issues or discussions.
- [ ] I'm sure this feature is not implemented.
- [ ] I'm sure it's a reasonable and popular requirement.

### Description of the feature / 需求描述

这个功能很实用，希望大佬能添加这个功能

### Suggested solution / 实现思路

_No response_

### Additional context / 附件

_No response_


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 701bbc2ff3b..8140f325a9b 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
     - [x] WebDav(Support OneDrive/SharePoint without API)
     - [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
     - [x] [Mediatrack](https://www.mediatrack.cn/)
-    - [x] [139yun](https://yun.139.com/) (Personal, Family)
+    - [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
     - [x] [YandexDisk](https://disk.yandex.com/)
     - [x] [BaiduNetdisk](http://pan.baidu.com/)
     - [x] [Terabox](https://www.terabox.com/main)
diff --git a/README_cn.md b/README_cn.md
index 7e45d60f757..5c71ccce4c3 100644
--- a/README_cn.md
+++ b/README_cn.md
@@ -58,7 +58,7 @@
     - [x] WebDav(支持无API的OneDrive/SharePoint)
     - [x] Teambition（[中国](https://www.teambition.com/ )，[国际](https://us.teambition.com/ )）
     - [x] [分秒帧](https://www.mediatrack.cn/)
-    - [x] [和彩云](https://yun.139.com/) (个人云, 家庭云)
+    - [x] [和彩云](https://yun.139.com/) (个人云, 家庭云，共享群组)
     - [x] [Yandex.Disk](https://disk.yandex.com/)
     - [x] [百度网盘](http://pan.baidu.com/)
     - [x] [UC网盘](https://drive.uc.cn)
diff --git a/README_ja.md b/README_ja.md
index 453e7b9966b..cd4446fab8e 100644
--- a/README_ja.md
+++ b/README_ja.md
@@ -58,7 +58,7 @@
     - [x] WebDav(Support OneDrive/SharePoint without API)
     - [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
     - [x] [Mediatrack](https://www.mediatrack.cn/)
-    - [x] [139yun](https://yun.139.com/) (Personal, Family)
+    - [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
     - [x] [YandexDisk](https://disk.yandex.com/)
     - [x] [BaiduNetdisk](http://pan.baidu.com/)
     - [x] [Terabox](https://www.terabox.com/main)
diff --git a/drivers/139/driver.go b/drivers/139/driver.go
index 2fedc477730..8862983ce5e 100644
--- a/drivers/139/driver.go
+++ b/drivers/139/driver.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"path"
 	"strconv"
 	"strings"
 	"time"
@@ -14,15 +15,16 @@ import (
 	"github.com/alist-org/alist/v3/internal/driver"
 	"github.com/alist-org/alist/v3/internal/errs"
 	"github.com/alist-org/alist/v3/internal/model"
-	"github.com/alist-org/alist/v3/pkg/utils"
 	"github.com/alist-org/alist/v3/pkg/cron"
+	"github.com/alist-org/alist/v3/pkg/utils"
+	"github.com/alist-org/alist/v3/pkg/utils/random"
 	log "github.com/sirupsen/logrus"
 )
 
 type Yun139 struct {
 	model.Storage
 	Addition
-	cron   *cron.Cron
+	cron    *cron.Cron
 	Account string
 }
 
@@ -56,6 +58,11 @@ func (d *Yun139) Init(ctx context.Context) error {
 			d.RootFolderID = "root"
 		}
 		fallthrough
+	case MetaGroup:
+		if len(d.Addition.RootFolderID) == 0 {
+			d.RootFolderID = d.CloudID
+		}
+		fallthrough
 	case MetaFamily:
 		decode, err := base64.StdEncoding.DecodeString(d.Authorization)
 		if err != nil {
@@ -96,6 +103,8 @@ func (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
 		return d.getFiles(dir.GetID())
 	case MetaFamily:
 		return d.familyGetFiles(dir.GetID())
+	case MetaGroup:
+		return d.groupGetFiles(dir.GetID())
 	default:
 		return nil, errs.NotImplement
 	}
@@ -108,9 +117,11 @@ func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
 	case MetaPersonalNew:
 		url, err = d.personalGetLink(file.GetID())
 	case MetaPersonal:
-		fallthrough
-	case MetaFamily:
 		url, err = d.getLink(file.GetID())
+	case MetaFamily:
+		url, err = d.familyGetLink(file.GetID(), file.GetPath())
+	case MetaGroup:
+		url, err = d.groupGetLink(file.GetID(), file.GetPath())
 	default:
 		return nil, errs.NotImplement
 	}
@@ -154,8 +165,22 @@ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
 				"accountType": 1,
 			},
 			"docLibName": dirName,
+			"path":       path.Join(parentDir.GetPath(), parentDir.GetID()),
+		}
+		pathname := "/orchestration/familyCloud-rebuild/cloudCatalog/v1.0/createCloudDoc"
+		_, err = d.post(pathname, data, nil)
+	case MetaGroup:
+		data := base.Json{
+			"catalogName": dirName,
+			"commonAccountInfo": base.Json{
+				"account":     d.Account,
+				"accountType": 1,
+			},
+			"groupID":      d.CloudID,
+			"parentFileId": parentDir.GetID(),
+			"path":         path.Join(parentDir.GetPath(), parentDir.GetID()),
 		}
-		pathname := "/orchestration/familyCloud/cloudCatalog/v1.0/createCloudDoc"
+		pathname := "/orchestration/group-rebuild/catalog/v1.0/createGroupCatalog"
 		_, err = d.post(pathname, data, nil)
 	default:
 		err = errs.NotImplement
@@ -176,6 +201,34 @@ func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj,
 			return nil, err
 		}
 		return srcObj, nil
+	case MetaGroup:
+		var contentList []string
+		var catalogList []string
+		if srcObj.IsDir() {
+			catalogList = append(catalogList, srcObj.GetID())
+		} else {
+			contentList = append(contentList, srcObj.GetID())
+		}
+		data := base.Json{
+			"taskType":    3,
+			"srcType":     2,
+			"srcGroupID":  d.CloudID,
+			"destType":    2,
+			"destGroupID": d.CloudID,
+			"destPath":    dstDir.GetPath(),
+			"contentList": contentList,
+			"catalogList": catalogList,
+			"commonAccountInfo": base.Json{
+				"account":     d.Account,
+				"accountType": 1,
+			},
+		}
+		pathname := "/orchestration/group-rebuild/task/v1.0/createBatchOprTask"
+		_, err := d.post(pathname, data, nil)
+		if err != nil {
+			return nil, err
+		}
+		return srcObj, nil
 	case MetaPersonal:
 		var contentInfoList []string
 		var catalogInfoList []string
@@ -246,6 +299,65 @@ func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) e
 			pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo"
 		}
 		_, err = d.post(pathname, data, nil)
+	case MetaGroup:
+		var data base.Json
+		var pathname string
+		if srcObj.IsDir() {
+			data = base.Json{
+				"groupID":           d.CloudID,
+				"modifyCatalogID":   srcObj.GetID(),
+				"modifyCatalogName": newName,
+				"path":              srcObj.GetPath(),
+				"commonAccountInfo": base.Json{
+					"account":     d.Account,
+					"accountType": 1,
+				},
+			}
+			pathname = "/orchestration/group-rebuild/catalog/v1.0/modifyGroupCatalog"
+		} else {
+			data = base.Json{
+				"groupID":     d.CloudID,
+				"contentID":   srcObj.GetID(),
+				"contentName": newName,
+				"path":        srcObj.GetPath(),
+				"commonAccountInfo": base.Json{
+					"account":     d.Account,
+					"accountType": 1,
+				},
+			}
+			pathname = "/orchestration/group-rebuild/content/v1.0/modifyGroupContent"
+		}
+		_, err = d.post(pathname, data, nil)
+	case MetaFamily:
+		var data base.Json
+		var pathname string
+		if srcObj.IsDir() {
+			// 网页接口不支持重命名家庭云文件夹
+			// data = base.Json{
+			// 	"catalogType": 3,
+			// 	"catalogID":   srcObj.GetID(),
+			// 	"catalogName": newName,
+			// 	"commonAccountInfo": base.Json{
+			// 		"account":     d.Account,
+			// 		"accountType": 1,
+			// 	},
+			// 	"path": srcObj.GetPath(),
+			// }
+			// pathname = "/orchestration/familyCloud-rebuild/photoContent/v1.0/modifyCatalogInfo"
+			return errs.NotImplement
+		} else {
+			data = base.Json{
+				"contentID":   srcObj.GetID(),
+				"contentName": newName,
+				"commonAccountInfo": base.Json{
+					"account":     d.Account,
+					"accountType": 1,
+				},
+				"path": srcObj.GetPath(),
+			}
+			pathname = "/orchestration/familyCloud-rebuild/photoContent/v1.0/modifyContentInfo"
+		}
+		_, err = d.post(pathname, data, nil)
 	default:
 		err = errs.NotImplement
 	}
@@ -303,6 +415,28 @@ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
 		pathname := "/hcy/recyclebin/batchTrash"
 		_, err := d.personalPost(pathname, data, nil)
 		return err
+	case MetaGroup:
+		var contentList []string
+		var catalogList []string
+		// 必须使用完整路径删除
+		if obj.IsDir() {
+			catalogList = append(catalogList, obj.GetPath())
+		} else {
+			contentList = append(contentList, path.Join(obj.GetPath(), obj.GetID()))
+		}
+		data := base.Json{
+			"taskType":    2,
+			"srcGroupID":  d.CloudID,
+			"contentList": contentList,
+			"catalogList": catalogList,
+			"commonAccountInfo": base.Json{
+				"account":     d.Account,
+				"accountType": 1,
+			},
+		}
+		pathname := "/orchestration/group-rebuild/task/v1.0/createBatchOprTask"
+		_, err := d.post(pathname, data, nil)
+		return err
 	case MetaPersonal:
 		fallthrough
 	case MetaFamily:
@@ -337,10 +471,12 @@ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
 					"account":     d.Account,
 					"accountType": 1,
 				},
+				"sourceCloudID":     d.CloudID,
 				"sourceCatalogType": 1002,
 				"taskType":          2,
+				"path":              obj.GetPath(),
 			}
-			pathname = "/orchestration/familyCloud/batchOprTask/v1.0/createBatchOprTask"
+			pathname = "/orchestration/familyCloud-rebuild/batchOprTask/v1.0/createBatchOprTask"
 		}
 		_, err := d.post(pathname, data, nil)
 		return err
@@ -536,21 +672,20 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
 		}
 		pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest"
 		if d.isFamily() {
-			// data = d.newJson(base.Json{
-			// 	"fileCount":    1,
-			// 	"manualRename": 2,
-			// 	"operation":    0,
-			// 	"path":         "",
-			// 	"seqNo":        "",
-			// 	"totalSize":    0,
-			// 	"uploadContentList": []base.Json{{
-			// 		"contentName": stream.GetName(),
-			// 		"contentSize": 0,
-			// 		// "digest": "5a3231986ce7a6b46e408612d385bafa"
-			// 	}},
-			// })
-			// pathname = "/orchestration/familyCloud/content/v1.0/getFileUploadURL"
-			return errs.NotImplement
+			data = d.newJson(base.Json{
+				"fileCount":    1,
+				"manualRename": 2,
+				"operation":    0,
+				"path":         path.Join(dstDir.GetPath(), dstDir.GetID()),
+				"seqNo":        random.String(32), //序列号不能为空
+				"totalSize":    0,
+				"uploadContentList": []base.Json{{
+					"contentName": stream.GetName(),
+					"contentSize": 0,
+					// "digest": "5a3231986ce7a6b46e408612d385bafa"
+				}},
+			})
+			pathname = "/orchestration/familyCloud-rebuild/content/v1.0/getFileUploadURL"
 		}
 		var resp UploadResp
 		_, err := d.post(pathname, data, &resp)
diff --git a/drivers/139/types.go b/drivers/139/types.go
index 42b939bf69d..c34cba0388b 100644
--- a/drivers/139/types.go
+++ b/drivers/139/types.go
@@ -7,6 +7,7 @@ import (
 const (
 	MetaPersonal    string = "personal"
 	MetaFamily      string = "family"
+	MetaGroup       string = "group"
 	MetaPersonalNew string = "personal_new"
 )
 
@@ -54,6 +55,7 @@ type Content struct {
 	//ContentDesc     string      `json:"contentDesc"`
 	//ContentType     int         `json:"contentType"`
 	//ContentOrigin   int         `json:"contentOrigin"`
+	CreateTime string `json:"createTime"`
 	UpdateTime string `json:"updateTime"`
 	//CommentCount    int         `json:"commentCount"`
 	ThumbnailURL string `json:"thumbnailURL"`
@@ -196,6 +198,27 @@ type QueryContentListResp struct {
 	} `json:"data"`
 }
 
+type QueryGroupContentListResp struct {
+	BaseResp
+	Data struct {
+		Result struct {
+			ResultCode string `json:"resultCode"`
+			ResultDesc string `json:"resultDesc"`
+		} `json:"result"`
+		GetGroupContentResult struct {
+			ParentCatalogID string `json:"parentCatalogID"` // 根目录是"0"
+			CatalogList     []struct {
+				Catalog
+				Path string `json:"path"`
+			} `json:"catalogList"`
+			ContentList []Content `json:"contentList"`
+			NodeCount   int       `json:"nodeCount"` // 文件+文件夹数量
+			CtlgCnt     int       `json:"ctlgCnt"`   // 文件夹数量
+			ContCnt     int       `json:"contCnt"`   // 文件数量
+		} `json:"getGroupContentResult"`
+	} `json:"data"`
+}
+
 type ParallelHashCtx struct {
 	PartOffset int64 `json:"partOffset"`
 }
diff --git a/drivers/139/util.go b/drivers/139/util.go
index 5918e4c5305..ccb6a912f32 100644
--- a/drivers/139/util.go
+++ b/drivers/139/util.go
@@ -13,9 +13,9 @@ import (
 
 	"github.com/alist-org/alist/v3/drivers/base"
 	"github.com/alist-org/alist/v3/internal/model"
+	"github.com/alist-org/alist/v3/internal/op"
 	"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"
@@ -220,10 +220,11 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
 			"sortDirection": 1,
 		})
 		var resp QueryContentListResp
-		_, err := d.post("/orchestration/familyCloud/content/v1.0/queryContentList", data, &resp)
+		_, err := d.post("/orchestration/familyCloud-rebuild/content/v1.2/queryContentList", data, &resp)
 		if err != nil {
 			return nil, err
 		}
+		path := resp.Data.Path
 		for _, catalog := range resp.Data.CloudCatalogList {
 			f := model.Object{
 				ID:       catalog.CatalogID,
@@ -232,6 +233,7 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
 				IsFolder: true,
 				Modified: getTime(catalog.LastUpdateTime),
 				Ctime:    getTime(catalog.CreateTime),
+				Path:     path, // 文件夹上一级的Path
 			}
 			files = append(files, &f)
 		}
@@ -243,6 +245,7 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
 					Size:     content.ContentSize,
 					Modified: getTime(content.LastUpdateTime),
 					Ctime:    getTime(content.CreateTime),
+					Path:     path, // 文件所在目录的Path
 				},
 				Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
 				//Thumbnail: content.BigthumbnailURL,
@@ -257,6 +260,61 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
 	return files, nil
 }
 
+func (d *Yun139) groupGetFiles(catalogID string) ([]model.Obj, error) {
+	pageNum := 1
+	files := make([]model.Obj, 0)
+	for {
+		data := d.newJson(base.Json{
+			"groupID":         d.CloudID,
+			"catalogID":       catalogID,
+			"contentSortType": 0,
+			"sortDirection":   1,
+			"startNumber":     pageNum,
+			"endNumber":       pageNum + 99,
+			"path":            catalogID,
+		})
+
+		var resp QueryGroupContentListResp
+		_, err := d.post("/orchestration/group-rebuild/content/v1.0/queryGroupContentList", data, &resp)
+		if err != nil {
+			return nil, err
+		}
+		path := resp.Data.GetGroupContentResult.ParentCatalogID
+		for _, catalog := range resp.Data.GetGroupContentResult.CatalogList {
+			f := model.Object{
+				ID:       catalog.CatalogID,
+				Name:     catalog.CatalogName,
+				Size:     0,
+				IsFolder: true,
+				Modified: getTime(catalog.UpdateTime),
+				Ctime:    getTime(catalog.CreateTime),
+				Path:     catalog.Path, // 文件夹的真实Path， root:/开头
+			}
+			files = append(files, &f)
+		}
+		for _, content := range resp.Data.GetGroupContentResult.ContentList {
+			f := model.ObjThumb{
+				Object: model.Object{
+					ID:       content.ContentID,
+					Name:     content.ContentName,
+					Size:     content.ContentSize,
+					Modified: getTime(content.UpdateTime),
+					Ctime:    getTime(content.CreateTime),
+					Path:     path, // 文件所在目录的Path
+				},
+				Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
+				//Thumbnail: content.BigthumbnailURL,
+			}
+			files = append(files, &f)
+		}
+		if pageNum > resp.Data.GetGroupContentResult.NodeCount {
+			break
+		}
+		pageNum = pageNum + 100
+	}
+	return files, nil
+}
+
 func (d *Yun139) getLink(contentId string) (string, error) {
 	data := base.Json{
 		"appName":   "",
@@ -273,6 +331,32 @@ func (d *Yun139) getLink(contentId string) (string, error) {
 	}
 	return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
 }
+func (d *Yun139) familyGetLink(contentId string, path string) (string, error) {
+	data := d.newJson(base.Json{
+		"contentID": contentId,
+		"path":      path,
+	})
+	res, err := d.post("/orchestration/familyCloud-rebuild/content/v1.0/getFileDownLoadURL",
+		data, nil)
+	if err != nil {
+		return "", err
+	}
+	return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
+}
+
+func (d *Yun139) groupGetLink(contentId string, path string) (string, error) {
+	data := d.newJson(base.Json{
+		"contentID": contentId,
+		"groupID":   d.CloudID,
+		"path":      path,
+	})
+	res, err := d.post("/orchestration/group-rebuild/groupManage/v1.0/getGroupFileDownLoadURL",
+		data, nil)
+	if err != nil {
+		return "", err
+	}
+	return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
+}
 
 func unicode(str string) string {
 	textQuoted := strconv.QuoteToASCII(str)


Test patch: