Introduction.
I am developing a small cat-shaped robot “Mia”, that speaks various funny phrases in dialects
In the previous article, we implemented the following part of the calendar integration process.
This time, we would like to implement a system that accesses the Google Calendar API and periodically retrieves events for all users.
Overall flow
The Google API does not allow direct use of refresh_token
access to the API. Instead, you must use refresh_token
to issue a temporary access_token
and send a request to the API using this token.
This article implements this process in the following four steps
- Get Access Token using Refresh Token
- Access Google Calendar API to retrieve events
- Automation of event acquisition (construction of periodic execution scheduler)
- Outputs acquired events to log
flowchart
[refresh_token] --> [Google Token Endpoint] --> [access_token]
[access_token] --> [Google Calendar API] --> [イベント取得]
Get Access Token using Refresh Token
First, implement a function to obtain an access_token
from Google’s authentication server using refresh_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
}
Explanation of what is in the code
Get client_id and
client_secret
from environment variables: Google’s OAuth 2.0 authentication requires client_id
and client_secret
, so they are read from the environment variables.
clientID := os.Getenv("GOOGLE_CLIENT_ID")
clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
if clientID == "" || clientSecret == "" {
return "", fmt.Errorf("環境変数 GOOGLE_CLIENT_ID または GOOGLE_CLIENT_SECRET が設定されていません")
}
Create a request payload containing a refresh token and send Han TTP request to Google’s token endpoint. Decode the response into an AccessTokenResponse
structure to obtain an access token. Based on the access token, the event is retrieved from the Google Calendar API.
Automation of event acquisition (building a scheduler)
Use cron
library to perform event retrieval every 5 minutes. All users with refresh tokens are retrieved from the database in order to retrieve target users dynamically.
// 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: Scheduler implementation
Below is the scheduler task for Google Calendar added to main.go
Retrieve Google Calendar appointments that start within 5 minutes and output them to the log.
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,
)
}
}
}
operation check
The above code resulted in the following log output every 5 minutes.
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"}
The part of getting events from Google Calendar API using refresh_token
in the table has been implemented. The next step is to add a function to synthesize speech based on the text information of the acquired events and send it to the main body of Mia (ESP32) via device shadow.