🚀
【ミーア】Google Calendar APIをGoで操作する方法:Refresh Tokenからイベント取得まで
はじめに
様々な方言を話す。おしゃべりに小型ロボット「ミーア」を開発中。
前回、こちらの記事で、Flutterアプリからカレンダー連携ボタンを押して、googleサインインを実行したのちに、カレンダーのアクセスを許可すると、サーバー側に下記のようにrefreshtokenが保存される部分までを実装した。
今回は、refresh_token
を使ってGoogle Calendar APIにアクセスし、全ユーザーのイベントを定期的に取得するシステムを構築するところまでを実装したいと思う。
全体の流れ
Google APIでは、refresh_token
を直接利用してAPIにアクセスすることはできない。代わりに、refresh_token
を使用して一時的な access_token
を発行し、このトークンを使ってAPIにリクエストを送る必要がある。
この記事では、以下の4ステップでこの処理を実装する
- Refresh Tokenを使ってAccess Tokenを取得
- Google Calendar APIにアクセスしてイベントを取得
- イベント取得の自動化(定期実行スケジューラの構築)
- 取得したイベントをログに出力
フロー図
[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_id
とclient_secret
を取得**:**GoogleのOAuth 2.0認証では、
続きはこちらで記載しています。
Discussion