LoginSignup
1
0

go言語でGoogleカレンダーを操作する便利なサンプルコード(codeを標準入力しなくてよい版)

Posted at

はじめに

go言語でGoogleカレンダーを操作する方法について、2023年12月時点の公式のクイックスタートのサンプルコードが不便だったため、本ブログではより便利なサンプルコードを紹介します。

(この投稿はGO Inc. Advent Calendar 2023の一部です)

クイックスタートのサンプルコードは何が面倒か

GoogleカレンダーをGO言語で操作しようとしてインターネットを検索すると、一番最初にでてくるのは公式のクイックスタートのサンプルコードだとおもいます。

ですが、このコード、初回実行時に認証コードを切り取って標準入力に渡す部分がとても面倒です

具体的には、手順どおりに作業して、go言語のプログラムを書いて実行すると、コンソールに以下のように指示が出て、標準入力待ちになります。

Go to the following link in your browser then type the authorization code: 
https://accounts.google.com/o/oauth2/auth?access_type=offline&client_id=xxx.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.readonly&state=state-token

これが何を言ってるかというと、このURLをブラウザに入力し、ブラウザで認証を行い、認証後のリダイレクトURLにあるcodeの値を取得し、コンソールの標準入力に入力しろってことです。

指示通りブラウザにURLを入力し、認証するまでは良いのですが、不便なのはこの後です。

認証後の画面は、以下のような「localhostに到達できません」といったエラーメッセージの画面がでます。

image.png

この画面をみると認証が失敗したのかなと思いますが、そうではありません。

よく見るとブラウザのURL欄には以下のようなURLが記載されているはずです。

http://localhost/?state=state-token&code=XXX&scope=https://www.googleapis.com/auth/calendar.readonly

実は、このURLのパラメータのcode=XXXXXXの部分をコピーし、標準入力待ちしているコンソールに入力しろという指示でした。

localhostに到達できないというエラーが出ていたのはURLのサーバがlocalhostを指定しているためでした。

codeを標準入力に入れることでプログラムは先に進み、トークンが保存された旨が表示され、所望のカレンダーの予定が表示されます。

Saving credential file to: token.json
Upcoming events:
出社 (2023-12-20T09:00:00+09:00)
会議 (2023-12-20T10:00:00+09:00)
...

このように、かなりわかりにくく不便なコードになっています。

より便利なサンプルコード

ではどうすべきかというと、以下のコードを実行して下さい

package main

import (
	"context"
	"encoding/gob"
	"fmt"
	"hash/fnv"
	"log"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"time"

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

func osUserCacheDir() string {
	switch runtime.GOOS {
	case "darwin":
		return filepath.Join(os.Getenv("HOME"), "Library", "Caches")
	case "linux", "freebsd":
		return filepath.Join(os.Getenv("HOME"), ".cache")
	}
	log.Printf("TODO: osUserCacheDir on GOOS %q", runtime.GOOS)
	return "."
}

func tokenCacheFile(config *oauth2.Config) string {
	hash := fnv.New32a()
	hash.Write([]byte(config.ClientID))
	hash.Write([]byte(config.ClientSecret))
	hash.Write([]byte(strings.Join(config.Scopes, " ")))
	fn := fmt.Sprintf("go-api-demo-tok%v", hash.Sum32())
	return filepath.Join(osUserCacheDir(), url.QueryEscape(fn))
}

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

func saveToken(file string, token *oauth2.Token) {
	f, err := os.Create(file)
	if err != nil {
		log.Printf("Warning: failed to cache oauth token: %v", err)
		return
	}
	defer f.Close()
	gob.NewEncoder(f).Encode(token)
}

func newOAuthClient(ctx context.Context, config *oauth2.Config) *http.Client {
	cacheFile := tokenCacheFile(config)
	token, err := tokenFromFile(cacheFile)
	if err != nil {
		token = tokenFromWeb(ctx, config)
		saveToken(cacheFile, token)
	} else {
		log.Printf("Using cached token %#v from %q", token, cacheFile)
	}

	return config.Client(ctx, token)
}

func tokenFromWeb(ctx context.Context, config *oauth2.Config) *oauth2.Token {
	ch := make(chan string)
	randState := fmt.Sprintf("st%d", time.Now().UnixNano())
	ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		if req.URL.Path == "/favicon.ico" {
			http.Error(rw, "", 404)
			return
		}
		if req.FormValue("state") != randState {
			log.Printf("State doesn't match: req = %#v", req)
			http.Error(rw, "", 500)
			return
		}
		if code := req.FormValue("code"); code != "" {
			fmt.Fprintf(rw, "<h1>Success</h1>Authorized.")
			rw.(http.Flusher).Flush()
			ch <- code
			return
		}
		log.Printf("no code")
		http.Error(rw, "", 500)
	}))
	defer ts.Close()

	config.RedirectURL = ts.URL
	authURL := config.AuthCodeURL(randState)
	go openURL(authURL)
	log.Printf("Authorize this app at: %s", authURL)
	code := <-ch
	log.Printf("Got code: %s", code)

	token, err := config.Exchange(ctx, code)
	if err != nil {
		log.Fatalf("Token exchange error: %v", err)
	}
	return token
}

func openURL(url string) {
	try := []string{"xdg-open", "google-chrome", "open"}
	for _, bin := range try {
		err := exec.Command(bin, url).Run()
		if err == nil {
			return
		}
	}
	log.Printf("Error opening URL in browser.")
}

func valueOrFileContents(value string, filename string) string {
	if value != "" {
		return value
	}
	slurp, err := os.ReadFile(filename)
	if err != nil {
		log.Fatalf("Error reading %q: %v", filename, err)
	}
	return strings.TrimSpace(string(slurp))
}

func main() {
	ctx := context.Background()
	b, err := os.ReadFile("credentials.json")
	if err != nil {
		log.Fatalf("Unable to read client secret file: %v", err)
	}

	// If modifying these scopes, delete your previously saved token.json.
	config, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope)
	if err != nil {
		log.Fatalf("Unable to parse client secret file to config: %v", err)
	}
	c := newOAuthClient(ctx, config)

	srv, err := calendar.NewService(ctx, option.WithHTTPClient(c))
	if err != nil {
		log.Fatalf("Unable to retrieve Calendar client: %v", err)
	}

	t := time.Now().Format(time.RFC3339)
	events, err := srv.Events.List("primary").ShowDeleted(false).
		SingleEvents(true).TimeMin(t).MaxResults(10).OrderBy("startTime").Do()
	if err != nil {
		log.Fatalf("Unable to retrieve next ten of the user's events: %v", err)
	}
	fmt.Println("Upcoming events:")
	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
			}
			fmt.Printf("%v (%v)\n", item.Summary, date)
		}
	}
}

このコードは、 google-api-go-client のレポジトリにあるサンプルコードをもとに作りました。

このコードを実行すると、ブラウザで認証した後に以下のように「Success Authorized」と表示される画面に遷移し、先程のコードで発生した標準入力にcodeを入力する手間が不要ことがわかります!!

image.png

仕組みとしては、localhostに一時的にWebサーバを立てて、リダイレクトURLを処理している作りになっています。

このコードを使って、便利なGoogleカレンダーAPIライフを送ってください!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0