はじめに
様々な方言を話す。おしゃべりに小型ロボット「ミーア」を開発中。
前回、こちらの記事で、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認証では、client_id
と client_secret
が必須なので、環境変数から読み込んでいる。
clientID := os.Getenv("GOOGLE_CLIENT_ID")
clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
if clientID == "" || clientSecret == "" {
return "", fmt.Errorf("環境変数 GOOGLE_CLIENT_ID または GOOGLE_CLIENT_SECRET が設定されていません")
}
リフレッシュトークンを含んだリクエストペイロードを作成し、Googleのトークンエンドポイントに対してHTTPリクエストを送信。レスポンスをAccessTokenResponse
構造体にデコードし、アクセストークンを取得。アクセストークンをもとに、Google Calendar APIからイベントを取得するという流れ。
イベント取得の自動化(スケジューラの構築)
cron
ライブラリを使って、5分おきにイベント取得を実行する。対象ユーザーを動的に取得するために、リフレッシュトークンを持つ全ユーザーをデータベースから取得。
// GoogleCalendarTokenServiceに全ユーザー取得メソッドを追加
func (s *GoogleCalendarTokenService) GetAllTokens(ctx context.Context) ([]mia.GoogleCalendarToken, error) {
var tokens []mia.GoogleCalendarToken
query := `
SELECT user_id, refresh_token, created_at, updated_at
FROM google_calendar_tokens
`
err := s.db.SelectContext(ctx, &tokens, query)
if err != nil {
return nil, fmt.Errorf("failed to fetch all tokens: %w", err)
}
return tokens, nil
}
main.go:スケジューラの実装
以下は、main.go
に追加したGoogle Calendar用のスケジューラタスク
5分以内に開始するGoogleカレンダーの予定を取得し、ログに出力する。
func main(){
// Google Calendar Token Service の初期化
googleTokenService := rdb.NewGoogleCalendarTokenService(db)
googleTokenValidator, err := google.NewGoogleCalendarTokenValidator()
if err != nil {
slog.Error("Failed to initialize Google Calendar Token Validator", "error", err)
os.Exit(1)
}
// Google Calendar Serviceの初期化
googleCalendarService := google.NewGoogleCalendarService()
// 5分ごとにGoogle Calendarのイベントを取得するタスク
c.AddFunc("0 */5 * * * *", func() {
ctx := context.Background()
fetchGoogleCalendarEvents(ctx, googleTokenService, googleCalendarService)
})
}
// google calendar eventsを取得
func fetchGoogleCalendarEvents(
ctx context.Context,
googleTokenService *rdb.GoogleCalendarTokenService,
googleCalendarService *google.GoogleCalendarService,
) {
slog.Info("Starting Google Calendar events fetch task")
// データベースから対象ユーザーを取得
userTokens, err := googleTokenService.GetAllTokens(ctx)
if err != nil {
slog.Error("Failed to fetch user tokens from database", "error", err)
return
}
for _, token := range userTokens {
// リフレッシュトークンを使ってアクセストークンを取得
accessToken, err := googleCalendarService.GetAccessToken(token.RefreshToken)
if err != nil {
slog.Error("Failed to fetch Google Calendar access token", "userID", token.UserID, "error", err)
continue
}
// Google Calendar APIからイベントを取得
now := time.Now().UTC()
fiveMinutesLater := now.Add(5 * time.Minute)
events, err := googleCalendarService.GetEvents(ctx, accessToken, now, fiveMinutesLater)
if err != nil {
slog.Error("Failed to fetch Google Calendar events", "userID", token.UserID, "error", err)
continue
}
// イベントをログに出力
for _, event := range events {
slog.Info("Fetched Google Calendar event",
"userID", token.UserID,
"event", event.Summary,
"startTime", event.Start.DateTime,
)
}
}
}
動作確認
以上のコードにより、5分ごとに以下のようなログが出力された。
clocky_api_local | {"time":"2024-11-26T04:00:02.038511052Z","level":"INFO","msg":"Fetched Google Calendar event","userID":4,"event":"テスト:デザイン定例","startTime":"2024-11-26T13:00:00+09:00"}
テーブル内のrefresh_token
を使ってGoogle Calendar APIからイベントを取得する部分は実装完了。次のステップとして、取得したイベントのテキスト情報をもとに音声合成をして、デバイスシャドウ経由でミーア本体(ESP32)に送信する機能を追加する。