🚀

【ミーア】Google Calendar APIをGoで操作する方法:Refresh Tokenからイベント取得まで

2024/11/26に公開

はじめに

様々な方言を話す。おしゃべりに小型ロボット「ミーア」を開発中。

https://mia-cat.com/

前回、こちらの記事で、Flutterアプリからカレンダー連携ボタンを押して、googleサインインを実行したのちに、カレンダーのアクセスを許可すると、サーバー側に下記のようにrefreshtokenが保存される部分までを実装した。

https://kazulog.fun/dev/authcode-refreshtoken-calendar/

今回は、refresh_token を使ってGoogle Calendar APIにアクセスし、全ユーザーのイベントを定期的に取得するシステムを構築するところまでを実装したいと思う。

全体の流れ

Google APIでは、refresh_token を直接利用してAPIにアクセスすることはできない。代わりに、refresh_token を使用して一時的な access_token を発行し、このトークンを使ってAPIにリクエストを送る必要がある。

この記事では、以下の4ステップでこの処理を実装する

  1. Refresh Tokenを使ってAccess Tokenを取得
  2. Google Calendar APIにアクセスしてイベントを取得
  3. イベント取得の自動化(定期実行スケジューラの構築)
  4. 取得したイベントをログに出力

フロー図

[refresh_token] --> [Google Token Endpoint] --> [access_token]
[access_token] --> [Google Calendar API] --> [イベント取得]

Refresh Tokenを使ってAccess Tokenを取得

まずは refresh_token を利用してGoogleの認証サーバーから access_token を取得する関数を実装する。

mia/google/google_calendar_service.go

package google

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"time"

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

const TokenEndpoint = "https://oauth2.googleapis.com/token"

// AccessTokenResponse GoogleのトークンAPIレスポンス
type AccessTokenResponse struct {
	AccessToken string `json:"access_token"`
	ExpiresIn   int    `json:"expires_in"`
	TokenType   string `json:"token_type"`
	Scope       string `json:"scope"`
}

// GoogleCalendarService Google Calendar APIを利用するサービス
type GoogleCalendarService struct {
	httpClient  *http.Client
	accessToken string
	expiry      time.Time
}

// NewGoogleCalendarService 初期化
func NewGoogleCalendarService() *GoogleCalendarService {
	return &GoogleCalendarService{
		httpClient: &http.Client{},
	}
}

// GetAccessToken リフレッシュトークンを使ってアクセストークンを取得
func (s *GoogleCalendarService) GetAccessToken(refreshToken string) (string, error) {
	// トークンが有効であれば再利用
	if s.accessToken != "" && time.Now().Before(s.expiry) {
		return s.accessToken, nil
	}

	clientID := os.Getenv("GOOGLE_CLIENT_ID")
	clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")

	if clientID == "" || clientSecret == "" {
		return "", fmt.Errorf("環境変数 GOOGLE_CLIENT_ID または GOOGLE_CLIENT_SECRET が設定されていません")
	}

	payload := map[string]string{
		"client_id":     clientID,
		"client_secret": clientSecret,
		"refresh_token": refreshToken,
		"grant_type":    "refresh_token",
	}
	body, _ := json.Marshal(payload)

	req, err := http.NewRequest("POST", TokenEndpoint, bytes.NewBuffer(body))
	if err != nil {
		return "", fmt.Errorf("リクエスト作成エラー: %v", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := s.httpClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("HTTPリクエストエラー: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		responseBody, _ := ioutil.ReadAll(resp.Body)
		return "", fmt.Errorf("アクセストークン取得失敗: ステータスコード %d, レスポンス %s", resp.StatusCode, string(responseBody))
	}

	var tokenResp AccessTokenResponse
	if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
		return "", fmt.Errorf("レスポンスデコードエラー: %v", err)
	}

	s.accessToken = tokenResp.AccessToken
	s.expiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
	return s.accessToken, nil
}

// GetEvents 指定された時間範囲のイベントを取得
func (s *GoogleCalendarService) GetEvents(ctx context.Context, accessToken string, timeMin, timeMax time.Time) ([]*calendar.Event, error) {
	srv, err := calendar.NewService(ctx, option.WithTokenSource(oauth2.StaticTokenSource(&oauth2.Token{
		AccessToken: accessToken,
	})))
	if err != nil {
		return nil, fmt.Errorf("Google Calendar APIクライアント作成エラー: %v", err)
	}

	events, err := srv.Events.List("primary").
		ShowDeleted(false).
		SingleEvents(true).
		TimeMin(timeMin.Format(time.RFC3339)).
		TimeMax(timeMax.Format(time.RFC3339)).
		OrderBy("startTime").
		Do()
	if err != nil {
		return nil, fmt.Errorf("Google Calendar APIイベント取得エラー: %v", err)
	}

	return events.Items, nil
}

コードの中身を解説

環境変数からclient_idclient_secretを取得**:**GoogleのOAuth 2.0認証では、

続きはこちらで記載しています。
https://kazulog.fun/dev/google-calendar-refresh-token-cron/

Discussion