🦁

Hono x Cloudflare Workersを使ってGoogle Calendarの予定を取得してLINEに通知する

2024/12/20に公開

昔にGAS(Google Apps Script)を使ってGoogle Calendarの予定を取得してLINEに通知していました。
その際に使っていたのがLINE Notifyなのですが、2025年3月31日で使えなくなるということで、LINE Messaging APIを使って作り直してみました。

https://www.itmedia.co.jp/news/articles/2410/10/news147.html

使った技術・サービス

  • Hono
  • Cloudflare Workers
  • Google Calendar API
  • LINE Messaging API

Honoの名前をよく見かけるようになってきたのと、Cloudflare系のサービスを使ったことが無いかつ、サクッと作れそうだったのでCloudflare Workersで試してみました。

実装

実装したものはこちら↓
https://github.com/iwsknj/today-schedule-line-bot

準備

LINEに通知するために、通知用のLINE公式アカウントの作成と、LINE Messaging APIのトークンを取得する必要があります。
ここらへんの記事を参考にしました。

https://qiita.com/MikH/items/d9876b6e50f7c8510d0b

https://qiita.com/jksoft/items/4d57a9282a56c38d0a9c

https://zenn.dev/halt/articles/20241023_line_messaging

Google Calendar APIを使うために、GCPでGoogle Calendar APIを有効化 & サービスアカウント作成します。
こういうときって、OAuthで認証して、そのトークンを使ってAPIを叩くのが一般的だと思いますが、
自分だけしか使わないのでサービスアカウントを作成して、ターゲットのカレンダーにサービスアカウントを追加して使うようにしました。

https://developers.google.com/calendar/api/guides/concepts/service-accounts

https://www.cdata.com/jp/blog/googlecalendarserviceaccount

https://zenn.dev/yoshida567/scraps/dbe87a0c51c48c

Honoインストール

このコマンドを実行して使うテンプレートを選択したらアプリケーションが作成されます。

npm create hono@latest my-app

これでアプリケーションとCloudflare Workersにデプロイできる環境が作成されるのでとても便利です。

Google Calendar APIでイベント取得

サービスアカウントのクレデンシャルを使って、認証してそのトークンを使ってGoogle Calendar APIを叩きます。
このやり方なのでサクッとできる(これまでも使ったことがあるから)と思いましたが、Cloudflare Workersの実行環境だとNode.js標準モジュールが使えないので、googleapisというライブラリでの認証ができませんでした。

代替としてNode.jsに依存していないライブラリを使いました。

https://zenn.dev/kkenjii/articles/hono-cloudflare-worker-googleapi

その結果、このような実装になりました

  const scopes = ["https://www.googleapis.com/auth/calendar.readonly"];
  const googleAuth: GoogleKey = JSON.parse(env.GCP_SERVICE_ACCOUNT);
  const oauth = new GoogleAuth(googleAuth, scopes);
  const token = await oauth.getGoogleAuthToken();
  const calendar = google.calendar({ version: "v3" });
  const startOfDay = dayjs()
    .tz(TIME_ZONE)
    .subtract(15, "day")
    .startOf("day")
    .hour(0)
    .minute(0)
    .second(0);
  const endOfDay = dayjs()
    .tz(TIME_ZONE)
    .add(15, "day")
    .endOf("day")
    .hour(23)
    .minute(59)
    .second(59);
  const today = dayjs().tz(TIME_ZONE);
  const yesterday = dayjs().tz(TIME_ZONE).subtract(1, "day");
  const tomorrow = dayjs().tz(TIME_ZONE).add(1, "day");
  const res = await calendar.events.list({
    calendarId: env.GOOGLE_CALENDER_ID,
    timeMin: startOfDay.format(),
    timeMax: endOfDay.format(),
    singleEvents: true,
    orderBy: "startTime",
    oauth_token: token,
  });
  • env.GCP_SERVICE_ACCOUNTにはjson形式のテキストが入っている
  • カレンダーの予定を15日前後取得しているのは、その日をまたいでいる予定も取得して表示したいから
  • 認証は https://github.com/Schachte/cloudflare-google-auth を使って、そのトークンをAPI実行時に渡している

LINE Messaging APIで通知

ここは通知するテキストを作成して、各種トークンやシークレットをセットして、LINE Messaging APIを叩くだけでした。

// LINEへ送信
const client = new line.messagingApi.MessagingApiClient({
  channelAccessToken: env.LINE_CHANNEL_ACCESS_TOKEN,
});
line.middleware({
  channelSecret: env.LINE_CHANNEL_SECRET,
});
await client.pushMessage({
  to: env.LINE_USER_ID,
  messages: [{ type: "text", text: textMessages.join("\n") }],
});

ルーティングではなく、cronで実行

パブリックなリクエストではなく定期実行をしたかったのでcron triggerを使いました。

デフォルトの形から、

export default app

以下のように設定。mainが実行する関数です。

const scheduled: ExportedHandlerScheduledHandler<Env> = async (
  _event,
  env,
  ctx
) => {
  ctx.waitUntil(main(env));
};

export default {
  fetch: app.fetch,
  scheduled,
};

wrangler.tomlにスケジュールの設定を追加します。

[triggers]
crons = [ "0 22 * * *"]

※ 実行環境のタイムゾーンがUTCなので、cronの時間はUTCで設定します。日本時間の午前7時に実行されるようにしています

Day.jsで少し苦戦

実行環境のタイムゾーンはUTCでカレンダーの予定の日時はJSTだったので、少し取り扱いに苦戦しましたが、
オブジェクトに対して.tz()でタイムゾーンを指定することで期待通りの結果が得られました。

dayjs().tz(TIME_ZONE)

所感

  • 毎朝に予定をLINEで確認できるのが良い
    • Slack通知でも良いかも
  • Hono x Cloudflare Workersの開発 ~ デプロイがとてもスムーズで体験が良かった
  • 自分だけの用途で使うなら無料の範囲内で使えそうなのもよい
  • GCPのCloud functionsとかGASとかでちょろちょろ細かいコードを書いているので、ここにまとめるのも良さそう

Discussion