Goを使ってNotionカレンダーからGoogleカレンダーへデータを移動させてみる

# はじめに
NotionのカレンダーとGoogleカレンダーを両方つかっていましたが、
2つとも管理するのが煩わしくなってきたので、Googleカレンダーに統一しようと思いました。

そこでNotionAPIとGoogleCalendarAPIを使って、Notionカレンダー→Googleカレンダーへ
データを移動するプログラムをGoで作ってみました。

# それぞれのAPIの始め方
ひとまず両方ともAPIを触ったことがなかったので、公式のスタートガイドから概要を学びました。
スタートガイドとリファレンスについては下のリンクになります。
詳しいガイドの内容については割愛させて頂きます。

**NotionAPI**
[NotioAPIスタートガイド](https://developers.notion.com/docs)
[NotionAPIリファレンス](https://developers.notion.com/reference/intro)

**GoogleCalendarAPI**
[GoogleCalendarAPIクイックスタート](https://developers.google.com/calendar/api/quickstart/go)
[GoogleCalendarAPIリファレンス](https://developers.google.com/calendar/api/v3/reference)
[Goドキュメント](https://pkg.go.dev/google.golang.org/api/calendar/v3)

GoogleCalendarAPIを使う際、OAuth認証のトークン取得に少し手間取ったので以下のqita記事を参考にしました、
[Google API OAuth2.0のアクセストークン&リフレッシュトークン取得手順 2017年2月版](https://qiita.com/iwaseasahi/items/2363dc1d246bc06baeae/)

# 実装
### やりたい事
* 開始日と終了日を決めて範囲内のデータを移動させる
* すでにGoogleカレンダーに同名の項目が登録済みならば更新する

### 簡単な設計
最初にPlantUMLを使ってざっくりとシーケンス図を作ってみました。
この図に従って実装を行いました。
![NotionToCalender.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/724787/b4b35ed7-27c3-b5fc-7fd9-fa5672c785e7.png)

### 作成コード
全体のコードは以下GitHubリポジトリ参照
https://github.com/aki537/notion-to-google-calender

```golang: main.go
package main

import (
	"NotionToGoogleCalender/controller"
	"log"
)

const (
	// startとendに指定された間のカレンダーを登録する
	Start = "2021-10-01"
	End   = "2022-03-1"
)

func main() {
	// notionの情報を取得
	notionList, err := controller.GetNotionCalender(Start, End)
	if err != nil {
		log.Fatalf("Failed GetNotionCalender: %v", err)
	}

	// 取得した情報をgoogleカレンダーに登録
	err = controller.PutGoogleCalender(notionList)
	if err != nil {
		log.Fatalf("Failed PutGoogleCalender: %v", err)
	}
}
```
```go:controller/notion
package controller

import (
	"NotionToGoogleCalender/api"
	"fmt"
	"log"
	"time"
)

const (
	timeFormat = "2006-01-02"
)

// GetNotionCalender は、notionカレンダーから情報を取得して構造体を返します。
func GetNotionCalender(start, end string) ([]*AddCalender, error) {
	addCalenderList := []*AddCalender{}
	notionList := []*api.Result{}
	notion, err := api.NewNotion()
	if err != nil {
		return nil, err
	}

	rangeList := getSplitRange(start, end, []*Range{})
	// 2ヶ月ごとにページのリストを取得
	for _, item := range rangeList {
		reqBody, err := api.NewCalenderReqBody(item.Start, item.End)
		if err != nil {
			return nil, err
		}
		res, err := notion.FetchPageList(reqBody)
		if err != nil {
			return nil, err
		}
		notionList = append(notionList, res.Results...)
	}

	// ページの子ブロックを取得して構造体に追加
	for _, item := range notionList {
		childblock, err := notion.FetchChildrenBlock(item.ID)
		if err != nil {
			return nil, err
		}
		// 子ブロックのリストを変換して追加
		addCalender := toAddCalender(childblock.Results, item.Properties.Date.Date.Start)
		addCalenderList = append(addCalenderList, addCalender)
	}
	log.Printf("追加用構造体に変換完了 全%d件", len(addCalenderList))
	return addCalenderList, nil
}

// getSplitRangeは、取得したい日付範囲を2ヶ月ごとに分割して再帰的に取得します
func getSplitRange(start, end string, list []*Range) []*Range {
	startDate, _ := time.Parse(timeFormat, start)
	endDate, _ := time.Parse(timeFormat, end)

	// 開始日より2ヶ月プラスした日時
	addStartDate := startDate.AddDate(0, 2, 0)
	//  開始日より2ヶ月プラスした日時 < 終了日となっているか
	ok := addStartDate.Before(endDate)
	if ok {
		addStart := addStartDate.Format(timeFormat)
		list = append(list, &Range{Start: start, End: addStart})
		list = getSplitRange(addStart, end, list)
	} else {
		list = append(list, &Range{Start: start, End: end})
	}
	return list
}

// toAddCalender は、取得した子ブロックをgoogleカレンダー登録用の構造体に変換します。
func toAddCalender(list []*api.Result, date string) *AddCalender {
	result := &AddCalender{}

	// 追加する日付
	result.Date = date
	// タイトル
	dateTime, _ := time.Parse(timeFormat, date)
	result.Title = fmt.Sprintf("Diary %d/%d/%d", dateTime.Year(), dateTime.Month(), dateTime.Day())
	// 本文
	body := ""
	for _, item := range list {
		for _, text := range item.Paragraph.RichText {
			body += text.PlainText + "\n"
		}
	}
	result.Body = body

	return result
}
```
```go:controller/calendar
package controller

import (
	"NotionToGoogleCalender/api"
	"log"

	"google.golang.org/api/calendar/v3"
)

// PutGoogleCalender は、notionのカレンダーをgooglecalenderに登録します
// 登録済みの場合は更新します。
func PutGoogleCalender(addCalenderList []*AddCalender) error {
	calender, err := api.NewGoogleCalendar()
	if err != nil {
		return err
	}

	// notionのカレンダーをgooglecalenderに登録
	for _, item := range addCalenderList {
		log.Printf("カレンダー登録日：%s", item.Date)
		event := &calendar.Event{
			Summary:     item.Title,
			Description: item.Body,
			Start: &calendar.EventDateTime{
				Date: item.Date,
			},
			End: &calendar.EventDateTime{
				Date: item.Date,
			},
			ColorId: "2",
			Reminders: &calendar.EventReminders{
				UseDefault: false,
			},
		}
		// その日のイベントを取得し、同じタイトルがある場合は更新、無い場合は新しく挿入する
		dateList, err := calender.FetchDateEvents(item.Date)
		if err != nil {
			return err
		}
		eventId, ok := getEventID(dateList.Items, item.Title)
		if ok {
			log.Printf("カレンダー挿入 %s", event.Summary)
			err = calender.UpdateEvent(eventId, event)
			if err != nil {
				return err
			}
		} else {
			log.Printf("カレンダー更新 %s", event.Summary)
			err = calender.InsertEvent(event)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

// getEventID は、引数に渡したタイトルがイベント内にある場合にeventIDを返却します
func getEventID(eventItems []*calendar.Event, addTitle string) (string, bool) {
	for _, item := range eventItems {
		if item.Summary == addTitle {
			return item.Id, true
		}
	}
	return "", false
}
```
# さいごに
それぞれのAPIの仕様の把握や使いはじめに時間がかかりましたが、実装自体は
思っていたより簡単にできました。

今度GASを触ってみたいなあと思っているので、今回の実装をGASでもやってみようかなと思ってます。