我が家の救世主、Google Calendar連携Slack botをつくました

## この記事は
[Slack Advent Calendar 2018 - Qiita](https://qiita.com/advent-calendar/2018/slack) の14日目の記事です。

私は個人で使用しているSlackのワークスペースに、
[Google カレンダーと Slack を連携させる – Slack](https://get.slack.help/hc/ja/articles/206329808-Google-%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC%E3%81%A8-Slack-%E3%82%92%E9%80%A3%E6%90%BA%E3%81%95%E3%81%9B%E3%82%8B)　
これを使っているんですが、なんせ通知が来ても彼氏がシカトして予定を忘れやがる。

この窮地を救うため、聞けば返してくれるGoogle Calendar連携botくんを作成しました.

## botの概要

- "今日の予定は？"って聞くと一覧で出してくれる
<img width="873" alt="スクリーンショット 2018-12-11 13.33.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/174728/5840b8a1-60e3-a510-9f05-6ebffed255be.png">
- "今週の予定は？"って聞くと一覧で出してくれる
<img width="913" alt="スクリーンショット 2018-12-11 13.37.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/174728/114fb349-7679-e8c9-e761-5b32d7d5293d.png">

予定が丸見えです。




## 実装内容
[Slack API | Slack](https://api.slack.com/)　と [Calendar API  |  Google Developers](https://developers.google.com/calendar/) を使用します

**main.go**

```go

func main() {
	token := os.Getenv("SLACKBOT")
	api := slack.New(token)
	client, err := gcalendar.Authorize()
	if err != nil {
		logger.Errorf("can not google calendar API authorized")
	}

	// WebSocketでSlack RTM APIに接続する
	rtm := api.NewRTM()
	// goroutineで並列化する
	go rtm.ManageConnection()

	// イベントを取得する
	for msg := range rtm.IncomingEvents {
		// 型swtichで型を比較する
		switch ev := msg.Data.(type) {
		case *slack.MessageEvent:
			switch ev.Msg.Text {
			case "今週の予定は？":
				schedule, err := schedule.New(client, "week")
				if err != nil {
					logger.Errorf("get event error: %v", err)
				}
				rtm.SendMessage(rtm.NewOutgoingMessage(schedule, ev.Channel))
			case "今日の予定は？":
				schedule, err := schedule.New(client, "day")
				if err != nil {
					logger.Errorf("get event error: %v", err)
				}
				rtm.SendMessage(rtm.NewOutgoingMessage(schedule, ev.Channel))
			}
		case *slack.InvalidAuthEvent:
			log.Print("Invalid credentials")
		}
	}
}
```

**schedule.go**

```go

func New(c *http.Client, duration string) (string, error) {
	srv, err := calendar.New(c)
	if err != nil {
		return "", err
	}

	now := time.Now()
	var end time.Time
	switch duration {
	case "day":
		end = now.AddDate(0, 0, 1)
	case "week":
		end = now.AddDate(0, 0, 7)
	}
	if end.IsZero() {
		end = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
	}

	events, err := srv.Events.List("5t9khdis149i45g9a43dvg7vn8@group.calendar.google.com").
		ShowDeleted(false).SingleEvents(true).TimeMin(now.Format(time.RFC3339)).
		MaxResults(10).TimeMax(end.Format(time.RFC3339)).OrderBy("startTime").Do()
	if err != nil {
		log.Fatalf("Unable to retrieve next ten of the user's events: %v", err)
		return "", err
	}

	var messages []string
	if len(events.Items) == 0 {
		fmt.Println("No upcoming events found.")
	} else {
		for _, item := range events.Items {
			date := item.Start.DateTime
			if date == "" {
				date = item.Start.Date
			}
			messages = append(messages, fmt.Sprintf("%v: %v\n", date, item.Summary))
		}
	}

	return strings.Join(messages, ""), nil
}
```
gcalendar.go

```go

func Authorize() (*http.Client, error) {
	// If modifying these scopes, delete your previously saved token.json.
	b, err := ioutil.ReadFile("credentials.json")
	if err != nil {
		log.Fatalf("Unable to read client secret file: %v", err)
		return nil, err
	}
	config, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope)
	if err != nil {
		log.Fatalf("Unable to parse client secret file to config: %v", err)
	}
	// The file token.json stores the user's access and refresh tokens, and is
	// created automatically when the authorization flow completes for the first
	// time.
	tokFile := "token.json"
	tok, err := tokenFromFile(tokFile)
	if err != nil {
		tok = tokenFromWeb(config)
		saveToken(tokFile, tok)
	}
	return config.Client(context.Background(), tok), nil
}

func tokenFromFile(path string) (*oauth2.Token, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, errors.New("cannot open file")
	}
	defer f.Close()

	tok := &oauth2.Token{}
	err = json.NewDecoder(f).Decode(tok)
	return tok, err
}

// Request a token from the web, then returns the retrieved token.
func tokenFromWeb(config *oauth2.Config) *oauth2.Token {
	authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
	fmt.Printf("Go to the following link in your browser then type the "+
		"authorization code: \n%v\n", authURL)

	var authCode string
	if _, err := fmt.Scan(&authCode); err != nil {
		log.Fatalf("Unable to read authorization code: %v", err)
	}

	tok, err := config.Exchange(context.TODO(), authCode)
	if err != nil {
		log.Fatalf("Unable to retrieve token from web: %v", err)
	}
	return tok
}

// Saves a token to a file path.
func saveToken(path string, token *oauth2.Token) {
	fmt.Printf("Saving credential file to: %s\n", path)
	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		log.Fatalf("Unable to cache oauth token: %v", err)
	}
	defer f.Close()
	json.NewEncoder(f).Encode(token)
}
```
あとは ```go run main.go```するなり、buildして実行するなりで動きます。
SlackのAPI登録、GoogleCalendarAPI登録に関しては他の記事を参考になさってください。

## まとめ

現状、作ったbotアプリによる生活の向上は見えていません。ただの気休めでした。

ちなみに、[冒頭で紹介した現状を招いている素晴らしいアプリ](https://get.slack.help/hc/ja/articles/206329808-Google-%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC%E3%81%A8-Slack-%E3%82%92%E9%80%A3%E6%90%BA%E3%81%95%E3%81%9B%E3%82%8B)は、Slackで見るとこんな感じに表示してくれます。

![gcal_view](https://get.slack.help/hc/en-us/article_attachments/214886708/calnotification.png)

リマインダーも設定できるし、見た目も綺麗だし最高です。


###これからの課題
今回急いで作ったので、Slackの用意したアプリに近づけるため、これからも進化させてゆきたいと思います。

- 時間をもっとみやすく
- せっかくのbotなのでもっと愛嬌をもたせたい
- 時間の設定までしてあげたり
- Alexaと連携してみたり

日々精進します