GoでGoogleカレンダーに予定詰め込んでみた。

# 自己紹介

じゅんじゅんと言うニックネームで、関西を拠点に活動しているフロントエンドエンジニアです。

HAL大阪3回生です。（2017/04/24現在）

イベントや、勉強会に参加してるので是非お会いした際はお声掛けください！

#まえがき

Goの記事書いていますが、しっかりGo言語を始めたのは先週からです。

なので、構造的にいけてない実装などがあったりすると思いますが良ければプルリク投げてください！

#Go言語とは

![](https://blog.golang.org/gopher/header.jpg)

[https://golang.org](https://golang.org)

>Go言語は2009年にGoogleによって作られたオープンソースの静的言語で、Linux・Mac・WindowsやAndroidで動作します。

>最近では、Google App Engineで使うこともできます。

>- シンプルな言語構造
- コンパイル・実行速度がはやい
- 安全性が高い
- 同期処理が容易に行える
- オープンソースである

[出典](http://qiita.com/suin/items/22662f43b6a6e8728798)

#Google Calendar API

![スクリーンショット 2017-04-24 11.12.54.png](https://qiita-image-store.s3.amazonaws.com/0/90347/8c2615eb-98ca-20d7-3dc4-7d4c15f6572b.png)

[Google Calendar API | Google Developers](https://developers.google.com/google-apps/calendar/)

Google Calendarの機能を使うためのAPIです。

今回はCSV区切りのデータを元に一気にカレンダーへ予定を登録していこうと思います。

#google-api-go-client

[google-api-go-client](https://github.com/google/google-api-go-client)

google-api-go-clientは、Googleが作っているGoogle APIを扱うための言語別のライブラリです。今回は、生で書いてたり誰か他の人がつくってるクライアントを使ってる人もいますがこのライブラリを使いたいと思います。

# APIを有効にする

まずCalendar APIを使うためにコンソールでAPIを有効にしないといけません。


まずconsoleへ移動します。
[https://console.developers.google.com](https://console.developers.google.com/?hl=JA)

まずプロジェクトを選択するか、新しくプロジェクトを作ります。

![スクリーンショット 2017-04-24 11.30.19.png](https://qiita-image-store.s3.amazonaws.com/0/90347/a3a349c5-f333-106b-3588-2ec736e4d68c.png)

すると、どのAPIを有効にするか選択する画面がでてきます。

`Google Calendar API`を選択します。

![スクリーンショット 2017-04-24 11.31.23.png](https://qiita-image-store.s3.amazonaws.com/0/90347/d7e450fe-9dd7-5a72-e84a-a63454b4703a.png)

するとこの画面なので、有効にするボタンを押します。

その後、認証情報ボタンを押して認証情報を追加します。

![スクリーンショット 2017-04-24 11.33.49.png](https://qiita-image-store.s3.amazonaws.com/0/90347/550c0e02-a461-b99f-60cf-fbfe86cc4e0f.png)

使用するAPIは`Google Calendar API`になっていると思います。

APIを呼び出す場所は`ウェブサーバ`に設定しておきましょう。

使用するデータの種類は、`アプリケーションデータ`にしてGoogle App EngineかGoogle Compute Engineを使いますか？という問は、`いいえ`を選択します。

![スクリーンショット 2017-04-24 11.36.40.png](https://qiita-image-store.s3.amazonaws.com/0/90347/bac5c951-7b61-a1af-bd74-b182ca6e49f7.png)

サービスアカウント名を追記して役割を適当に設定して、次へを押します。

すると、jsonファイルをダウンロードさせられると思います。

しかし、今までの工程は流れなので作りましたが別の認証情報を追加しないといけません。

まず上のタブの`OAuth同意画面`を選択し、ユーザに表示するサービス名を任意に入力します。

![スクリーンショット 2017-04-24 11.41.21.png](https://qiita-image-store.s3.amazonaws.com/0/90347/c0ae71ca-14dc-ba95-d778-20810a8a5cf9.png)

これで保存を押します。

![スクリーンショット 2017-04-24 11.39.00.png](https://qiita-image-store.s3.amazonaws.com/0/90347/18a7c8b9-3456-cedc-0c35-d97d14aa2ed8.png)

こんな画面になったら、上に`認証情報を作成`というボタンがあるので`OAuthクライアントID`を選択して新しい認証情報を作ります。

![スクリーンショット 2017-04-24 11.43.14.png](https://qiita-image-store.s3.amazonaws.com/0/90347/f5e50018-e32a-e3ab-dc07-1e89adbb3039.png)

アプリケーションの種類は、`その他`を選択し作成を押して認証情報を作成します。

![スクリーンショット 2017-04-24 11.44.14.png](https://qiita-image-store.s3.amazonaws.com/0/90347/f3e05904-7b0c-45c8-22b8-65b9519a802f.png)

すると、OAuth認証情報が追加されていると思います。

その新しく追加された行の一番最後にダウンロードするボタンがあると思います。

ここでダウンロードされるファイルはとても重要なものです。

ダウンロードしたら自分の作業ディレクトリへ移動して、そのディレクトリへコピーします。

名前は`cliend_secret.json`と変えてください。

ここまでで、準備を終えました。

では、やっとGo言語かいて行きましょう！

#csvの読み込み

今回のディレクトリ構成は

```
main.go
|
|- Utils/
   |
   |- calendarUtils.go
   |- fileUtils.go
|- schedule.csv
|-client_secret.json
```

とします。

`schedule.csv`の形式は、

```csv
title,location,year,month,day,start,end

<例>
買い物,ヒカリエ,2017,04,24,13:00,14:00
```

とします！

`fileUtils.go`を書いていきます。

```go:fileUtils.go
package Utils

import(
  "encoding/csv"
  "log"
  "os"
)

func errorMessage(err error){
  if err != nil {
    log.Fatal("Error: ",err)
  }
}

func Read(filename string) [][]string {
  fr,err := os.Open(filename)
  errorMessage(err)
  defer fr.Close()

  r := csv.NewReader(fr)
  rows, err := r.ReadAll()
  errorMessage(err)

  return rows
}
```

これで、2次元配列としてcsvの内容を取得できます。

この`fileUtils.go`は、mainパッケージでないためgoで実行することができません。

この`Read`を呼ぶ実装を`main.go`にしていきましょう。

```go:main.go
package main

import (
  "fmt"

  "github.com/konojunya/generate-shedule-calendar/Utils"
)

func main(){
  datas := Utils.Read("<csvファイル名>")
  for _, data := range datas {
    fmt.Println("行一欄",data)
  }
}
```

この時、`main.go`ではimportに`github.com/konojunya/generate-schedule-calendar/Utils`があります。

これは、他人のモジュールとして`go get`で取得した時などに使う書き方です。

自分自身で作っている場合は、`$GOPATH/src`以下にコピーします。

今回の場合は、`$GOPATH/src/github.com/konojunya/generate-schedule-calendar/Utils`にコピーします。

実行方法は、コンパイルして実行する

```bash
$ go run main.go
```

か、自分でコンパイルして実行ファイルを実行する

```bash
$ go build main.go
./main
```

のどちらか好きな方で実行してください。

すると、行一欄が取得できたと思います。

#APIしばこう！

では、APIしばきましょう（）

APIの実装自体は`Utils/calendarUtils.go`に書いていきましょう。

まずはスケジュールの構造を書いておきます。

```go:calendarUtils.go
type Schedule struct {
  Title string
  Location string
  Year string
  Month string
  Day string
  Start string
  End string
}
```

これが予定を追加するのに、必要なものだとします。

Titleは、**予定の名前**

Locationは、**予定がある場所**

Yearは、**予定のある年**

Monthは、**予定がある月**

Dayは、**予定がある日**

Startは、**予定の開始時間**

Endは、**予定の終了時間**

です。

では、関数の実装をしていきます。

google-api-go-clientのサンプルを元につくっているので実装がかぶっている部分が少しあります。

まずパブリックな関数は、`SetSchedule`,`CreateEvent`の2つになります。

プライベートは関数は、`getClient`,`getTokenFromWeb`,`tokenCacheFile`,`tokenFromFile`,`saveToken`,`createEventData`の6つです。

では、パブリックな関数から実装していきます。

`SetSchedule`はさっき定義した構造体にスケジュールを追加していきます。

```go:calendarUtils.go
func SetSchedule(schedule []string) *Schedule {
  return &Schedule{
    Title: schedule[0],
    Location: schedule[1],
    Year: schedule[2],
    Month: schedule[3],
    Day: schedule[4],
    Start: schedule[5],
    End: schedule[6],  
  }
}
```

csvから呼んできた1行を引数にとってます。

返り値として、Scheduleを返します。

次に予定を追加する`CreateEvent`を実装します。

```go:calendarUtils.go
func CreateEvent(schedule *Schedule, calendarId string){
  ctx := context.Background()
  b, err := ioutil.ReadFile("client_secret.json")
  errorLog("Unable to read client secret file: ", err)

  config, err := google.ConfigFromJSON(b, calendar.CalendarScope)
  errorLog("Unable to parse client secret file to config: ", err)

  client := getClient(ctx, config)
  srv, err := calendar.New(client)
  errorLog("Unable to retrieve calendar Client: ", err)

  _, err = srv.Events.Insert(calendarId, createEventData(schedule)).Do()
  errorLog("Unable to create event. ", err)

  fmt.Println(schedule.Title)
}
```

ここで、`cliend_secret.json`というファイルを読み込んでいるのがわかると思います。

`srv.Events.Insert`というメソッドでイベントを追加しています。

`calendarId`は、自分のgmailアドレスだと思います。

この中で`createEventData`という関数がでてきています、これは僕が作った構造体の中身をGoogleのAPIのパラメータの形に整形し直すものです。

```go:calendarUtils.go
func createEventData(schedule *Schedule) *calendar.Event {
  start_datatime := schedule.Year + "-" + schedule.Month + "-" + schedule.Day + "T" + schedule.Start + ":00+09:00"
  end_datatime := schedule.Year + "-" + schedule.Month + "-" + schedule.Day + "T" + schedule.End + ":00+09:00"

  event := &calendar.Event{
    Summary:  schedule.Title,
    Location: schedule.Location,
    Start: &calendar.EventDateTime{
      DateTime: start_datatime,
      TimeZone: "Asia/Tokyo",
    },
    End: &calendar.EventDateTime{
      DateTime: end_datatime,
      TimeZone: "Asia/Tokyo",
    },
  }

  return event
}
```

では、全貌を一度見ておきましょう。

```go:calendarUtils.go
package Utils

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/user"
	"path/filepath"
	"time"

	"golang.org/x/net/context"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/calendar/v3"
)

const CALENDAR_ID string = "osaka.hal.iw13a727@gmail.com"

type Schedule struct {
	Title    string
	Location string
	Year     string
	Month    string
	Day      string
	Start    string
	End      string
}

func SetSchedule(schedule []string) *Schedule {
	return &Schedule{
		Title:    schedule[0],
		Location: schedule[1],
		Year:     schedule[2],
		Month:    schedule[3],
		Day:      schedule[4],
		Start:    schedule[5],
		End:      schedule[6],
	}
}

func CreateEvent(schedule *Schedule) {
	ctx := context.Background()
	b, err := ioutil.ReadFile("client_secret.json")
	errorLog("Unable to read client secret file: ", err)

	config, err := google.ConfigFromJSON(b, calendar.CalendarScope)
	errorLog("Unable to parse client secret file to config: ", err)

	client := getClient(ctx, config)
	srv, err := calendar.New(client)
	errorLog("Unable to retrieve calendar Client: ", err)

	_, err = srv.Events.Insert(CALENDAR_ID, createEventData(schedule)).Do()
	errorLog("Unable to create event. ", err)

	fmt.Println(schedule.Title)
}

func errorLog(message string, err error) {
	if err != nil {
		log.Fatalf(message, err)
	}
}

func getClient(ctx context.Context, config *oauth2.Config) *http.Client {
	cacheFile, err := tokenCacheFile()
	errorLog("Unable to get path to cached credential file.: ", err)

	tok, err := tokenFromFile(cacheFile)
	if err != nil {
		tok = getTokenFromWeb(config)
		saveToken(cacheFile, tok)
	}
	return config.Client(ctx, tok)
}

func getTokenFromWeb(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 code string
	if _, err := fmt.Scan(&code); err != nil {
		log.Fatalf("Unable to read authorization code %v", err)
	}

	tok, err := config.Exchange(oauth2.NoContext, code)
	errorLog("Unable to retrieve token from web: ", err)

	return tok
}

func tokenCacheFile() (string, error) {
	usr, err := user.Current()

	if err != nil {
		return "", err
	}

	tokenCacheDir := filepath.Join(usr.HomeDir, ".credentials")
	os.MkdirAll(tokenCacheDir, 0700)
	return filepath.Join(tokenCacheDir, url.QueryEscape("generate-schedule-calendar.json")), err
}

func tokenFromFile(file string) (*oauth2.Token, error) {
	f, err := os.Open(file)
	if err != nil {
		return nil, err
	}
	t := &oauth2.Token{}
	err = json.NewDecoder(f).Decode(t)
	defer f.Close()
	return t, err
}

func saveToken(file string, token *oauth2.Token) {
	fmt.Printf("Saving credential file to: %s\n", file)
	f, err := os.Create(file)
	errorLog("Unable to cache oauth token: ", err)

	defer f.Close()
	json.NewEncoder(f).Encode(token)
}

func createEventData(schedule *Schedule) *calendar.Event {

	start_datatime := schedule.Year + "-" + schedule.Month + "-" + schedule.Day + "T" + schedule.Start + ":00+09:00"
	end_datatime := schedule.Year + "-" + schedule.Month + "-" + schedule.Day + "T" + schedule.End + ":00+09:00"

	event := &calendar.Event{
		Summary:  schedule.Title,
		Location: schedule.Location,
		Start: &calendar.EventDateTime{
			DateTime: start_datatime,
			TimeZone: "Asia/Tokyo",
		},
		End: &calendar.EventDateTime{
			DateTime: end_datatime,
			TimeZone: "Asia/Tokyo",
		},
	}

	return event
}
```

`calendarUtils.go`が完成したら`main.go`から呼ぶように設定しないといけません。

```go:main.go
package main

import (
  "fmt"

  "github.com/konojunya/generate-schedule-calendar/Utils"
)

func main(){
  datas := Utils.Read("schedule.csv")

  for _, schedule := range datas {
    s := Utils.SetSchedule(schedule)
    Utils.CreateEvent(s)
  }

  fmt.Println("finished!")
}
```

これで実装をやりきったので、実行していきます。

```bash
$ cp -r <Utilsのpath> $GOPATH/src/github.com/konojunya/generate-schedule-calendar
```

として、importで呼ばれる先の更新を忘れないようにしてください。

```bash
$ go run main.go
```

or

```bash
$ go build main.go
$ ./main
```

で実行するとcsvのファイルを呼んで、calendar APIを使って予定が追加されていると思います。

#あとがき

Twitterしています。

是非フォローしてください！ [@konojunya](https://twitter.com/konojunya)

コードは[konojunya/generate-schedule-calendar](http://github.com/konojunya/generate-schedule-calendar)にあるので、参考にしてみてください。

プルリクをマージしているので、この記事の時点のコミットだと[ここ](https://github.com/konojunya/generate-schedule-calendar/tree/046dcd44ed8be0a37460b9640f79be1106689dcc)になります。