我們昨天完成了整個組合Carousel的部分,今天來把資料來源換成從Google Drive API取得。
首先,我們先看一下需要哪些資料
type NewFolderBubbleParam struct {
Type string // 資料夾類型
Name string // 資料夾名稱
Path string // 資料夾路徑
ID string // folderID
InsideFolderM map[string]string // 子資料夾,[id]name
FileM map[string]string // 檔案,[id]name
}
那我們就從下往上一一來做,先處理FileM
,我們到adapter的drive.go中,寫一個ListFilesByID()
的function,讓他根據folderID
回傳下面所有檔案的Map。
透過Q(fmt.Sprintf("'%s' in parents and mimeType != 'application/vnd.google-apps.folder'", folderID)
,我們去到指定資料夾並把檔案內容過濾掉屬於”資料夾”類型的檔案,剩下的就是我們要的檔案了,再透過Fields("files(id, name)")
取出對應的id跟檔案名稱。
// internal\adapter\google\drive.go
func (d *GoogleDrive) ListFilesByID(folderID string) (map[string]string, error) {
nameM := make(map[string]string)
r, err := d.Service.Files.List().Q(fmt.Sprintf("'%s' in parents and mimeType != 'application/vnd.google-apps.folder'", folderID)).
Fields("files(id, name)").Do()
if err != nil {
log.Printf("Unable to retrieve files: %v", err)
return nil, err
}
fmt.Println("Files:")
if len(r.Files) == 0 {
fmt.Println("No files found.")
} else {
for _, i := range r.Files {
nameM[i.Id] = i.Name
fmt.Printf("%s (%s)\n", i.Name, i.Id)
}
}
fmt.Println("nameM 共:", len(nameM), "個檔案")
return nameM, nil
}
接著處理InsideFolderM
,跟上面幾乎一樣的邏輯,只是這邊我們透過Q(fmt.Sprintf("'%s' in parents and mimeType='application/vnd.google-apps.folder'", folderID))
,讓他只去取得類型是”資料夾”的內容。
// internal\adapter\google\drive.go
func (d *GoogleDrive) ListFolderByID(folderID string) (map[string]string, error) {
nameM := make(map[string]string)
r, err := d.Service.Files.List().Q(fmt.Sprintf("'%s' in parents and mimeType='application/vnd.google-apps.folder'", folderID)).
Fields("files(id, name)").Do()
if err != nil {
log.Printf("Unable to retrieve folders: %v", err)
return nil, err
}
fmt.Println("Folders:")
if len(r.Files) == 0 {
fmt.Println("No folders found.")
} else {
for _, i := range r.Files {
nameM[i.Id] = i.Name
fmt.Printf("%s (%s)\n", i.Name, i.Id)
}
}
fmt.Println("nameM 共:", len(nameM), "個資料夾")
return nameM, nil
}
ID
的部分是從外部帶去做button的,所以我們接著來做Path
,Path
比較麻煩,因為Google Drive提供的API並沒有直接查詢資料夾所在路徑的功能,但有可以取得上層資料夾ID的功能,所以我們可以自己透過遞迴來實現。
// internal\adapter\google\drive.go
func (d *GoogleDrive) FindFolderPathByID(folderID string) (string, error) {
return d.findFolderPathByIDRecursively(folderID, "")
}
func (d *GoogleDrive) findFolderPathByIDRecursively(folderID string, currentPath string) (string, error) {
r, err := d.Service.Files.Get(folderID).Fields("id, name, parents,shared").Do()
if err != nil {
log.Printf("Unable to retrieve folder information: %v", err)
return "", err
}
currentPath = r.Name + "/" + currentPath
if len(r.Parents) > 0 {
return d.findFolderPathByIDRecursively(r.Parents[0], currentPath)
}
if r.Shared {
currentPath = "與我共用/" + currentPath
}
return currentPath, nil
}
我們透過在Fields
中取parents
來拿到上級資料夾的id,如果還拿的到上一層的parents
我們就不斷遞迴。比較特別的是在遞迴完後,我們會透過r.Shared
來判斷當前資料是否共用,因為在”與我共用”的那一塊他預設是直接在跟目錄下的,所以我們在開頭加上"與我共用/"
,這樣在看路徑的時候會比較清楚到底對應在Google Drive的哪裡。
接著Name
和Type
的部分也是從外部帶入的,所以我們可以來寫Service了。在開始之前為了比較好定義Type
,我們先到Domain去自定義Type的類型。
// internal\domain\drive\flex.go
type FolderType int
const (
PersonalFolder FolderType = iota
SharedFolder
OtherFolder
)
然後回到Service層,我們寫一個ListFolderCarousel()
會從外部傳入domainDrive.FolderType
,並且透過switch case判斷完類型之後,決定要用ListMyDriveFolders()
還是ListSharedFolders()
,也從這邊決定類型要顯示的名稱。剩下的就是拿到最外層資料夾的folderList
後,迴圈呼叫對應的adapter function,然後將值放進去NewFolderBubbleParam
就好哩。
// internal\app\service\drive\drive_service.go
func (dr *GoogleDriveService) ListFolderCarousel(ctx context.Context, lineID string, folderType domainDrive.FolderType) (*domainDrive.FolderCarousel, error) {
dToken, err := dr.driveServiceDynamodb.GetGoogleOAuthToken(lineID)
if err != nil {
log.Println(err)
return nil, err
}
tok := oauth2.Token{
AccessToken: dToken.AccessToken,
TokenType: dToken.TokenType,
RefreshToken: dToken.RefreshToken,
Expiry: dToken.Expiry,
}
d, err := dr.driveServiceGoogleOA.NewGoogleDrive(ctx, &tok)
if err != nil {
log.Println(err)
return nil, err
}
var folderList map[string]string
var folderTypeString string
switch folderType {
case domainDrive.PersonalFolder:
folderList, err = d.ListMyDriveFolders()
folderTypeString = "我的雲端硬碟"
case domainDrive.SharedFolder:
folderList, err = d.ListSharedFolders()
folderTypeString = "與我共用"
default:
return nil, errors.New("unsupported folder type")
}
if err != nil {
return nil, err
}
var params domainDrive.NewFolderCarouselParam
for folderID, name := range folderList {
path, err := d.FindFolderPathByID(folderID)
if err != nil {
log.Println(err)
return nil, err
}
insideFolderM, err := d.ListFolderByID(folderID)
if err != nil {
log.Println(err)
return nil, err
}
fileM, err := d.ListFilesByID(folderID)
if err != nil {
log.Println(err)
return nil, err
}
param := domainDrive.NewFolderBubbleParam{
Type: folderTypeString,
Name: name,
Path: path,
ID: folderID,
InsideFolderM: insideFolderM,
FileM: fileM,
}
params.BubbleParams = append(params.BubbleParams, param)
}
carousel := domainDrive.NewFolderCarousel(params)
return &carousel, err
}
最後我們回到callback,補上兩個條件,並透過domainDrive.PersonalFolder
和domainDrive.SharedFolder
來決定要回傳哪種Carousel。
// internal\router\api\v1\callback.go
if message.Text == "mydrive" {
lineID := event.Source.UserID
res, err := app.DriveService.ListFolderCarousel(ctx, lineID, domainDrive.PersonalFolder)
if err != nil {
log.Println(err)
return
}
if _, err := app.LineBotClient.ReplyMessage(
event.ReplyToken,
linebot.NewFlexMessage("測試Flex Carousel", res.CarouselContainer),
).Do(); err != nil {
log.Println(err)
return
}
}
if message.Text == "shared" {
lineID := event.Source.UserID
res, err := app.DriveService.ListFolderCarousel(ctx, lineID, domainDrive.SharedFolder)
if err != nil {
log.Println(err)
return
}
if _, err := app.LineBotClient.ReplyMessage(
event.ReplyToken,
linebot.NewFlexMessage("測試Flex Carousel", res.CarouselContainer),
).Do(); err != nil {
log.Println(err)
return
}
}
回到LineBot測試一下,可以看到能夠正確取得我們GoogleDrive上的資料,並且組合出對應的Carousel了。那我們今天就先寫到這裡,明天是最後一天,補上button的功能應該就可以告一段落了~