開発チームがお届けするブログリレーです!既に公開されている記事もありますので、こちらから他のメンバーの投稿もぜひチェックしてみてください!

はじめに

こんにちは、第一開発事業部の森田です。

突然ですが、皆さんは仕事に関する毎朝のルーティーンって何かありますか?

私の場合、朝一番にGoogle Calendarを開いて、その日の予定をざっと頭に入れるようにしています。
ただ、日中に何度もカレンダーを見返すことは少ないので、うっかり時間を勘違いしていて、予定に遅れそうになったこともありました。

どうしたら防げるかと考えたときに、Slackの個人チャンネルにGoogle Calendarに設定されているその日の予定を朝一で通知するSlackbotを作ろうと考えました。
Google Calendarを1日に何回か見ればいいじゃんとも思いましたが、私は今まで個人開発というものをやったことがなかったので、これを機に挑戦してみました。

システム構成

以下が今回作成するSlackbotのシステム構成図です。

Slackbot構成図

Google Calendarから1日のスケジュールを取得するためにGoogle Calendar APIを使用します。

Google Calendarから取得したデータを加工し、個人チャンネルに1日の予定を投稿するためのLambdaを作成します。
また、平日の朝に毎日実行したいので、EventBridgeを使用して定期実行を行います。

Google Calendar APIを使うためのあれこれ

Google Calendar APIとは

公式ドキュメントには下記のように説明されています。

Google Calendar API は RESTful API であり、明示的な HTTP リクエストを通じて、または Google クライアントライブラリを使用してアクセスできます。この API は、Google Calendar の Web インターフェースで利用できるほとんどの機能を提供しています。
https://developers.google.com/workspace/calendar/api/guides/overview より)

Google CalendarのWebサイトで使用できる機能のほとんどをAPI経由で使用できます。
例えば、イベントの取得・追加・変更・削除などの機能をAPIを叩くことで実行できます。
また、イベントへのアクセス制御を行うこともできます。

Google Calendar APIを呼び出せるようになるまで

①Google Calendar APIを有効化する
Google Cloud Consoleから「Google Calendar API」と検索し、APIを有効化する

Google Calendar API有効化

②サービスアカウントを作成する
「認証情報>認証情報を作成」からサービスアカウントを選択します。

サービスアカウント作成

設定画面で、一意なサービスアカウント名を設定し、サービスアカウントを作成します。

サービスアカウントの詳細画面から「鍵」を選択します。
その後「キーを追加」から「新しい鍵を作成」を選択し、JSONタイプの鍵をダウンロードします。

JSONキーの作成

③Googleカレンダー側で設定を行う
次にGoogleカレンダー側で予定を共有するための設定を行います。
マイカレンダーから共有したいカレンダーを選択し、「設定と共有」を押下します。

カレンダーの設定と共有

「共有する相手」に先ほど作成したサービスアカウントのIDを追加します。

共有する相手の追加

また、同一タブ内にあるカレンダーの統合からカレンダーIDも控えておきます。
カレンダーIDは個人用のカレンダーであれば yourname@gmail.com 、共有用のカレンダーであれば ランダムな英数字@group.calendar.google.com となります。

これで、サービスアカウントに対してGoogleカレンダーを共有することが出来るようになりました。

Lambdaを作る

Googleカレンダーから1日の予定を取得し、読みやすい形に直してSlackに投稿するLambda関数を作ります。

簡単にコードの解説をしておきます。
lambda_handlerでGoogle Calendar APIを叩き、Slackのチャンネルに投稿したい形にデータを整形します。
post_to_slackでSlack APIを叩いて指定したチャンネルへ投稿を行います。
コード直貼りだと危なそうな情報は環境変数に格納しています。
必要なモジュールは適宜Lambdaレイヤーに追加してください。

⚙️環境変数

"GOOGLE_CREDENTIALS" : サービスアカウントのJSONキー
"CALENDAR_ID" : 控えておいたカレンダーID
"SLACK_BOT_TOKEN" : Slack APIを使うための認証トークン(「SlackとLambdaを連携させる」で登場)

🧑‍💻コード本体

import os, json, requests
from datetime import datetime, timedelta
from google.oauth2 import service_account
from googleapiclient.discovery import build

SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"]
TIMEZONE_OFFSET = 9  # JST

def lambda_handler(event, context):
    creds_info = json.loads(os.environ["GOOGLE_CREDENTIALS"])
    credentials = service_account.Credentials.from_service_account_info(
        creds_info, scopes=SCOPES
    )
    calendar_id = os.environ["CALENDAR_ID"]
    service = build("calendar", "v3", credentials=credentials)

    # JST で当日 00:00〜23:59 を求める
    now_utc = datetime.utcnow()<br>
    jst_now = now_utc + timedelta(hours=TIMEZONE_OFFSET)
    start = datetime(jst_now.year, jst_now.month, jst_now.day) - timedelta(hours=TIMEZONE_OFFSET)
    end   = start + timedelta(days=1)<br><br>

    # 1日のイベントを開始時間順で取得
    events = (
        service.events()
        .list(
            calendarId=calendar_id,
            timeMin=start.isoformat() + "Z",
            timeMax=end.isoformat() + "Z",
            singleEvents=True,
            orderBy="startTime",
        )
        .execute()
        .get("items", [])
    )

    # イベントが無い場合 
    if not events:
        post_to_slack("📅 スケジュールされた今日の予定はありません。")
        return {"statusCode": 200, "body": "No events"}

    # Slackに投稿するメッセージを整形する
    lines = ["📅 *今日の予定*:"]
    for ev in events:
        # イベントの開始・終了時刻を取得、終日の場合も考慮
        s_raw = ev["start"].get("dateTime") or ev["start"].get("date")
        e_raw = ev["end"].get("dateTime")   or ev["end"].get("date")

        try:<br>
            s_fmt = datetime.fromisoformat(s_raw).strftime("%H:%M")
            e_fmt = datetime.fromisoformat(e_raw).strftime("%H:%M")
            lines.append(f"- {s_fmt} – {e_fmt}:{ev.get('summary','(無題)')}")
        except ValueError:
            lines.append(f"- (終日){ev.get('summary','(無題)')}")

    # Slackへ投稿する関数の呼び出し
    post_to_slack("\n".join(lines))

    return {"statusCode": 200, "body": "Posted"}

def post_to_slack(text, channel="time-1st-morita"):
    token = os.environ["SLACK_BOT_TOKEN"]

    # Slack APIを叩く
    rsp = requests.post(
        "https://slack.com/api/chat.postMessage",
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
        json={"channel": channel, "text": text, "mrkdwn": True},
        timeout=10,
    )<br>
    body = rsp.json()
    if not body.get("ok"):
        raise RuntimeError(f"Slack error: {body}")

SlackとLambdaを連携させる

SlackチャンネルにLambdaからメッセージを送信するためにはSlack APIを使用する必要があります。

大まかな手順として

  1. Slack Appを作る(名前とかアイコンの設定)
  2. Slack Appに対する権限を設定をして、トークンを発行する
  3. WorkSpaceにSlack Appを追加して、任意のチャンネルにAppを追加する

という3ステップを踏むことでLambdaからSlackチャンネルに投稿が行えるようになります。

手順としては下記の記事がとてもシンプルかつ分かりやすくまとまっているので、詳しい手順についてはこちらを参照してください。

SlackAppの作り方 - Qiita
これは何?仕事で「なんらかのプログラムからSlackに通知する」ということを良くやるのですが(ほとんどがIIncoming Webhooksを利用)、その都度「SlackAppの作成って何するん…

ということで、今回作成するSlackbotを「予定管理くん」と命名しました。
アイコンは、しごできそうな画像にしてみました!

Slack Appアイコン

諸々の設定を済ませてチャンネルにAppを追加できたら、Lambdaの環境変数に発行したBot Tokenを格納していきます。

また、この時トークンの先頭の文字がxoxb-となっていることを確認してください。
Slack APIのトークンにはいくつかの種類があり、トークンの先頭の4文字はその種類を表しています。
利用用途の異なるトークンを使うと権限不足でエラーが出てしまうので、注意してください。

Token types
A tour of token types and their permission models, cornerstones of working with the Slack platform.

これでLambdaからSlackへ投稿を行う準備が出来ました。
Lambdaを起動して投稿が行えるか確認してみましょう。

Lambdaを実行してみましょう。
お、投稿できたみたいです。

投稿成功(Lambda)

次にSlackチャンネルを確認します。

投稿成功(Slack)

お、投稿できたみたいですね!
成功です。

EventBridgeでLambdaを定期実行する

Slackへスケジュールが投稿できることが確認できたので、最後に定期実行出来るようにしていきます。

EventBridgeでスケジュールを作成します。
スケジュールの設定画面で「定期的なスケジュール」を選択し、cronベースのスケジュールを設定していきます。
平日の朝10時に永続的にSlackチャンネルに投稿して欲しいので、cron式としてcron(0 10 ? * MON-FRI *)を設定します。
そんなに朝イチに投稿されなくてもいいかなと思っているので、フレックスタイムウィンドウは10分にしておきます。

EventBridgeの設定

その後、EventBridgeスケジュールのルールのターゲットとして先ほど作成したLambda関数を設定します。

LambdaとEventBridgeの連携

次の画面ではスケジュールの細かい設定が出来ますが、ここでは「スケジュール完了後のアクション」をNONEに設定するだけで他は特にいじらなくて大丈夫です。

その後、スケジュールの作成へ進みます。

これで定期実行が出来るようになったはずです。
平日の朝10時になるのを待ちます。

zzz…

zz…

z…

あ、朝10時になったみたいですね!Slackを確認してみましょう。

定期実行成功

無事に投稿されていました!
成功です。

おわりに

今回はGoogleカレンダーからその日の予定を引っ張ってきてSlackに投稿するSlackbotを作成してみました!

初めて個人開発をしてみましたが、自分の作ったアプリって愛着が湧くもんですね。
また、Slack上の色んなところで見かけるSlackbotってこんな風に作ってるんだということが分かってとても勉強になりました。

これからも時間があるときに個人開発をしてみたいと思います。

ここまで読んでくださりありがとうございました!