golangで、 google calendar api v3 を使ってカレンダーを操作する



概要
----

googleカレンダーをgolangで取得、追加、更新をやってみます。
[Goolge Calendar API v3](https://godoc.org/code.google.com/p/google-api-go-client/calendar/v3)を使うのですが、OAuthなので、まず以下の前準備が必要です。

1. google APIの Client IDの取得
1. OAuth認証でアクセストークンを取得する

ただし、アクセストークンの取得部分は
[goauth2の認証手順をもっと簡単に](http://d.hatena.ne.jp/taknb2nch/20140226/1393394578)を参考に、プログラム側で作りました。


環境
----

* OS X 10.9 (or Windows7 まだ動作確認していないけど、多少の手直しで動くはず)
* golang 1.3


google API Client IDの取得
---------------------


1. https://console.developers.google.com/project から、
```Create Project```をクリック
それぞれ適当な名前をつけて```Create```
![New Project](https://qiita-image-store.s3.amazonaws.com/0/26566/5609ac59-fc03-acff-e591-ac38f62ea882.png "スクリーンショット 2014-07-19 2.33.54.png")

2. Project Dashboardから ```Enable an API```
Calendar APIのStatusを```ON```に
![API status](https://qiita-image-store.s3.amazonaws.com/0/26566/df70a809-a833-cb34-b3a7-707d62f0f5c9.png "スクリーンショット 2014-07-19 2.40.22.png")

3. 左のメニューの```Credentials```から ```Create new Client ID```
![Credentials](https://qiita-image-store.s3.amazonaws.com/0/26566/9e38ea9e-473a-4caf-1de2-05265bdd069d.png "スクリーンショット 2014-07-19 2.42.33.png")
```Installed application```と```Other```を選択し```Create Client ID```
![Create Client ID](https://qiita-image-store.s3.amazonaws.com/0/26566/99c3ccca-0e7c-5dfd-5a09-7a33f59b0198.png "スクリーンショット 2014-07-19 2.47.20.png")

4. 「Client ID for native application」の```Download JSON```からClient Secretをダウンロード
![Download JSON](https://qiita-image-store.s3.amazonaws.com/0/26566/64a91876-da0f-b29a-63af-26c1bc131c2b.png "スクリーンショット 2014-07-19 2.50.43.png")

2. OAuth認証でアクセストークンを取得する
-----------------------------------

[goauth2の認証手順をもっと簡単に](http://d.hatena.ne.jp/taknb2nch/20140226/1393394578)を参考に、以下の手順を自動処理します。

1. リダイレクト先となるlocalhostにhttpサーバーを起動
1. ClientIDとClientSecretを使って、認証ページURLを生成
1. ブラウザを起動し認証ページのURLを開く
1. ID/Passの入力(ここは手動)
1. ブラウザのリダイレクトで、localhostに帰ってきたGETリクエストから認証コードを取得

```go:lib/google-auth.go
package lib

import (
	"fmt"
	"net"
	"net/http"
	"os/exec"
	"strings"
	"time"

	"code.google.com/p/goauth2/oauth"
)

type LocalServerConfig struct {
	Port    int
	Timeout int
	OS      string
}

type RedirectResult struct {
	Code string
	Err  error
}

type Redirect struct {
	Result      chan RedirectResult
	ServerStart chan bool
	ServerStop  chan bool
	Listener    net.Listener
}

// 各種OSでのブラウザ起動コマンドとURLのエスケープコード置換文字列
type OpenBrowser struct {
	EscapeAnd string
	arg       []string
}
var openBrowser = map[string]OpenBrowser{
	"windows": {`&`, []string{"cmd", "/c", "start"}},
	"darwin":  {`&`, []string{"open", "-a", "safari"}},
	"test1":   {`&`, []string{"echo", "", ""}},
	"test2":   {`&`, []string{"fugafuga", "", ""}},
}

func NewRedirect(result chan RedirectResult) *Redirect {
	return &Redirect{result, make(chan bool, 1), make(chan bool, 1), nil}
}

type AuthToken interface {
	GetTokenCache() error
	GetAuthCodeURL() string
	GetAuthToken(string) error
}

type GoogleToken struct {
	Transport *oauth.Transport
}

// テストしやすいようにAuth系APIを隠蔽する
func (this *GoogleToken) GetTokenCache() error {
	_, err := this.Transport.Config.TokenCache.Token()
	return err
}
func (this *GoogleToken) GetAuthCodeURL() string {
	return this.Transport.Config.AuthCodeURL("")
}
func (this *GoogleToken) GetAuthToken(code string) error {
	_, err := this.Transport.Exchange(code)
	return err
}

// アクセストークンを取得
func GoogleOauth(transport AuthToken, localServerConfig LocalServerConfig) (err error) {

	// キャッシュからトークンファイルを取得
	err = transport.GetTokenCache()
	if err == nil {
		return
	}
	url := transport.GetAuthCodeURL()
	code, err := getAuthCode(url, localServerConfig)
	if err != nil {
		err = fmt.Errorf("Error getAuthCode: %#v", err)
		return
	}
	// 認証トークンを取得する。（取得後、キャッシュへ）
	err = transport.GetAuthToken(code)
	if err != nil {
		err = fmt.Errorf("Exchange: %#v", err)
	}
	return
}

// アクセスコード取得
func (this *Redirect) GetCode(w http.ResponseWriter, r *http.Request) {
	//defer this.Listener.Stop()
	code := r.URL.Query().Get("code")

	if code == "" {
		fmt.Fprintf(w, `Erorr`)
		this.Result <- RedirectResult{Err: fmt.Errorf("codeを取得できませんでした。")}
		return
	}

	fmt.Fprintf(w, `<!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> </head>
			<body onload="window.open('about:blank','_self').close();">ブラウザが自動で閉じない場合は手動で閉じてください。</body>
			</html> `)
	this.Result <- RedirectResult{Code: code}
}

// localhostのhttpサーバー
func (this *Redirect) Server(port int) {
	mux := http.NewServeMux()
	mux.HandleFunc("/", this.GetCode)
	host := fmt.Sprintf("localhost:%d", port)
	fmt.Printf("Start Listen: %s\n", host)
	var err error
	this.Listener, err = net.Listen("tcp", host)
	if err != nil {
		this.Result <- RedirectResult{Err: err}
		return
	}
	server := http.Server{}
	server.Handler = mux
	go server.Serve(this.Listener)
	this.ServerStart <- true
	<-this.ServerStop
	this.Listener.Close()
	this.Result <- RedirectResult{Err: err}
	return
}
func (this *Redirect) Stop() {
	this.ServerStop <- true
}

// サーバー起動 -> ブラウザ起動 -> コード取得
func getAuthCode(url string, localServerConfig LocalServerConfig) (string, error) {

	var cmd *exec.Cmd

	//os := runtime.GOOS
	os := localServerConfig.OS
	var browser *OpenBrowser
	for key, value := range openBrowser {
		if os == key {
			browser = &value
			break
		}
	}
	if browser == nil {
		return "", fmt.Errorf("まだ未対応です・・・\n%s\n", url)
	}

	redirect := NewRedirect(make(chan RedirectResult, 1))
	go redirect.Server(localServerConfig.Port)

	// set redirect timeout
	redirectTimeout := time.After(time.Duration(localServerConfig.Timeout) * time.Second)
	<-redirect.ServerStart

	url = strings.Replace(url, "&", browser.EscapeAnd, -1)
	// ブラウザ起動

	//fmt.Printf("%v %v %v %v", browser.arg[0], browser.arg[1], browser.arg[2], url)
	cmd = exec.Command(browser.arg[0], browser.arg[1], browser.arg[2], url)
	if err := cmd.Start(); err != nil {
		return "", fmt.Errorf("Error:  start browser: %v, browser: %v\n", err, browser)
	}

	defer redirect.Stop()
	var result RedirectResult

	select {
	case result = <-redirect.Result:
		//ブラウザ側の応答があればなにもしない
	case <-redirectTimeout:
		// タイムアウト
		return "", fmt.Errorf("リダイレクト待ち時間がタイムアウトしました")
	}

	if result.Err != nil {
		return "", fmt.Errorf("Error: リダイレクト: %v\n", result.Err)
	}

	fmt.Printf("code: %v\n", result.Code)

	return result.Code, nil
}
```

Calendar APIを叩く
-----------------

事前に console.developers.google.comから```Download JSON```で保存したファイルを```google.json``` としてカレントディレクトリに保存しておく。



```go:main.go
package main

import (
	"flag"
	"fmt"
	"log"
	"runtime"

	"code.google.com/p/goauth2/oauth"
	"github.com/masahide/get-cybozu-schedule/lib"
)

func main() {

	flag.Usage = lib.Usage
	flag.Parse()

	if *lib.Version {
		fmt.Printf("%s\n", lib.ShowVersion())
		return
	}

	// ClientID等を読み込む
	config, err := lib.Parse("google.json")
	if err != nil {
		log.Fatalf("Error Server: %v", err)
		return
	}
	

	port := 3000
	transport := oauth.Transport{
		Config: &oauth.Config{
			ClientId:     config.Installed.ClientID,
			ClientSecret: config.Installed.ClientSecret,
			RedirectURL:  fmt.Sprintf("%s:%d", "http://localhost", port),
			Scope:        "https://www.googleapis.com/auth/calendar",
			AuthURL:      config.Installed.AuthURL,
			TokenURL:     config.Installed.TokenURL,
			TokenCache:   oauth.CacheFile("cache.json"),
		},
	}
	
	// OAuthを実行
	err = lib.GoogleOauth(&lib.GoogleToken{&transport}, lib.LocalServerConfig{port, 30, runtime.GOOS})
	if err != nil {
		log.Fatalf("Error Server: %v", err)
		return
	}

	// ここからやっとカレンダーAPIを使い始める
	svc, err := calendar.New(transport.Client())
	if err != nil {
		log.Fatalf("Error calendar.New: %v", err)
		return
	}

	// カレンダー一覧を取得
	cl, err := svc.CalendarList.List().Do()
	if err != nil {
		log.Fatalf("Error CalendarList.List(): %v", err)
		return
	}

	fmt.Printf("--- Your calendars ---\n")
	for _, item := range cl.Items {
		fmt.Printf("%# v\n", item)
	}

}
```