LINEで予定送るだけで自動でカレンダー登録！！GeminiAPIを利用して自動カレンダー登録してくれるLINEbotを開発してみた

# 📅 カレンダー自動とうろくん

カレンダーに予定を登録するのがめんどくさい。LINEで予定の連絡をもらってもいちいちカレンダーに手動で入力するのは面倒。予定のタイトル、日時を別々に入力していくのはかったるい。そんな悩みを解決するために、予定の内容をLINEに送信するだけで自動でカレンダーに登録してくれるLINE Botを開発しました。

その名も、「カレンダー自動とうろくん」！

:::note info
「カレンダー自動とうろくん」とは、LINEで送信された自然言語のメッセージから予定情報を抽出し、Googleカレンダーに自動で登録するLINE Botです。
AWS Lambdaを活用したサーバーレスアーキテクチャで構築されており、ユーザーは複雑な操作なしに、日常会話のような文章でカレンダー登録が可能となっています！
:::

<div style="display: flex; justify-content: center; gap: 20px;">
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/22a4ba28-2953-49e2-9a52-487e881037a6.jpeg" alt="カレンダー自動とうろくん イメージ図1" width="350"/>
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/71ab89e5-0a3f-48cb-abb1-f9af200c2a12.jpeg" alt="カレンダー自動とうろくん イメージ図2" width="350"/>
</div>

ソースコードは

https://github.com/shoitsu/calendar_linebot


## 全体像

![システム構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/447ec7e4-5297-4cb1-9064-2a8597f43279.png)

1. ユーザーがLINEで予定の内容を送信
2. LINE Messaging APIからLambda関数URL経由でLambda関数が起動
3. Lambda関数がGemini APIを使用して自然言語から予定情報を抽出
4. 抽出された情報をGoogle Calendar APIを使ってカレンダーに登録
5. 処理結果をLINE経由でユーザーに返信



## 1. Googleカレンダー連携のための下準備

Googleカレンダーに自動で登録するために、GCPでサービスアカウントを作成し、Google Calendar APIを使用してカレンダー登録を行います。

### 1-1. Google Calendar APIの有効化

1. [Google Cloud Platform](https://console.cloud.google.com/welcome "GCP")に移動します
2. 「APIとサービス」→「ライブラリ」を選択
3. 検索バーで「Google Calendar API」を検索
4. Google Calendar APIを選択して「有効にする」をクリック

### 1-2. サービスアカウントを作成

1. [Google Cloud Platform](https://console.cloud.google.com/welcome "GCP")に移動します
2. 「APIとサービス」を選択
3. 認証情報を選択し、「+認証情報を作成」からサービスアカウントを選択
   ![サービスアカウント作成画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/ff40f6e2-d66d-4489-ad30-d75f862017cf.png)
4. ロールをオーナーに設定してサービスアカウントを作成

### 1-3. 鍵の作成

1. 作成したサービスアカウントの一覧で、サービスアカウント名をクリック
2. 「鍵」タブを選択し、「キーを追加」→「新しい鍵を作成」をクリック
   ![鍵の作成画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/9d87b8f8-e94d-4571-8d2f-aa75035e569c.png)
3. キーのタイプは「JSON」を選択し、「作成」をクリック
4. 作成されたJSONファイルを`credentials.json`として保存しましょう（後で使います）

### 1-4. Googleカレンダーの設定

1. Googleカレンダーの設定を開きます
2. 予定を管理するマイカレンダーを選択→「特定のユーザーとの共有」の「ユーザーやグループを追加」をクリック
   ![カレンダー共有設定画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/dadfdc2b-2f57-4a51-9f52-5993e1d1a3f1.png)
3. 1-2で作成したサービスアカウントのメールアドレスを追加し、権限を「予定の変更」に設定
4. 同画面下部にあるカレンダーIDをメモしておきましょう（後で環境変数として使用します）

## 2. LINE Messaging APIのチャンネル作成

LINE Messaging APIの仕様が2024年9月に変更されたようで、他の記事を見ても最新の手順が見つからず苦労しました。ここでは、できるだけ詳しく手順を説明します。

まずは下記リンクから始めましょう：

https://developers.line.biz/ja/services/messaging-api/

### 2-1. LINE Developersコンソールにログイン

LINE Developersコンソールにログインします。
（初めての方は新規登録を完了させてください）
![LINE Developers ログイン画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/7502fa18-1118-42d7-b0bd-a2ab5aa0d0c0.png)

### 2-2. プロバイダーの作成

プロバイダーを作成します。
![プロバイダー作成画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/51a03828-e806-4947-af92-6dc743f34a07.png)

### 2-3. Messaging APIの設定

Messaging APIを選択します。
![Messaging API選択画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/b99845af-0b15-4c74-89bd-1e927444cdd1.png)

LINE DevelopersコンソールからMessaging APIチャンネルを直接作成することができなくなったようです（2024年9月頃から変更）。指示に従って公式アカウントを作成します。
![公式アカウント作成画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/4290b16d-2e50-4467-a03e-03ce7799bdb8.png)

### 2-4. LINE Official Account Managerでの設定

必要情報を入力して作成すると、LINE Official Account Managerコンソールが開きます。Messaging APIチャンネルを開設するために、画面右上部の設定を開きます。

![LINE Official Account Manager設定画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/b06abc14-59f8-4ab9-ad7d-9c4b4529f284.png)

左側のメニューバーにある歯車マークにカーソルを合わせると「Messaging API」が表示されます。
（このメニューを見つけるのに筆者は30分もかかりました。非常にわかりにくいインターフェースです）
![Messaging APIメニュー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/bf89e63d-f6a6-4013-9dfc-ccbe385ed9e7.png)

「Messaging APIを利用する」ボタンをクリックし、指示に従って進めていくとMessaging APIの開設が完了します。
![Messaging API設定画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/cdc58b79-62c4-4b1b-8bd2-1fe09d47f424.png)

### 2-5. チャンネルアクセストークンの取得

LINE Developersコンソールに戻り、作成したプロバイダーを選択すると、Messaging APIチャネルが表示されています。
（本記事ではプロバイダー名を「kozin_kaihatsu」、公式アカウント名を「カレンダー自動とうろくん」としています）
![プロバイダー画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/0279bf26-be67-49dd-a337-fade8047bee4.png)

Messaging APIチャネルに移動し、画面上部のメニューバーから「Messaging API設定」に移動します。ページ下部にスクロールすると「チャネルアクセストークン」があります。これを発行しておきましょう。
（このトークンはLambda関数でLINE Messaging APIを呼び出すときに使用します）
![チャネルアクセストークン発行画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557305/16a1ece2-01dc-4af6-9dab-9b1bfed71de6.png)

## 3. Lambdaの作成

今回はPython 3.9を使用しました。
（後述するライブラリのレイヤー作成時に、Python 3.12では問題が発生したため）
LINEからのWebhookイベントを直接Lambdaで受け取るため、「関数URL」を有効にし、「認証タイプ」は`NONE`を指定します。

### 3-1. ソースコード

<details>
<summary>lambda_function.py</summary>

```python
import os
import json
import requests
import datetime 
import google.auth 
from googleapiclient.discovery import build 

def lambda_handler(event, context):
    CHANNEL_ACCESS_TOKEN = os.environ["CHANNEL_ACCESS_TOKEN"]
    CALENDAR_ID = os.environ["CALENDAR_ID"] # 環境変数からカレンダーIDを取得

    print(event)
    body = json.loads(event["body"])
    replyToken = body["events"][0]["replyToken"]
    receivedText = body["events"][0]["message"]["text"]
    
    json_text = get_json_from_gemini(receivedText)
    
    # 予定追加処理
    success, message = add_google_calendar_event(json_text, CALENDAR_ID)

    # 返信メッセージを設定
    reply_message = message

    # LINE Messaging APIを使用してユーザーからのメッセージに対してリプライを送信する
    # HTTPリクエストでLINEのエンドポイントにJSONデータを送信
    url = "https://api.line.me/v2/bot/message/reply"
    header = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {CHANNEL_ACCESS_TOKEN}"
    }
    body = json.dumps({
        "replyToken": replyToken,
        "messages": [
            {
                "type": "text",
                "text": reply_message
            }
        ]
    })
    
    res = requests.post(url=url, headers=header, data=body)
    print(res.text)
    
    return "OK"

def get_json_from_gemini(received_message):
    """Gemini APIを使用してJSONを取得する関数"""
    try:
        # 環境変数からAPIキーを取得
        api_key = os.environ["GEMINI_API_KEY"]
        
        # 現在の日時を取得
        JST = datetime.timezone(datetime.timedelta(hours=+9), 'JST')
        today = datetime.datetime.now(JST).strftime("%Y-%m-%d %H:%M")
        
        # プロンプトテンプレート
        system_prompt = (
            "あなたの役割は、ユーザーが入力した日本語のテキストからイベント情報を抽出し、\n"
            "Googleカレンダーに登録できる形式のJSONを生成することです。\n\n"
        )
        user_prompt = (
            "入力から抽出すべき情報：\n"
            "1. 予定の概要（summary）\n"
            "2. 開始日時（start）\n"
            "3. 終了日時（end）\n"
            "4. タイムゾーン（\"Asia/Tokyo\" で固定）\n\n"
            "- JSONは以下の形式で出力してください。出力は**JSONのみ**とし、それ以外の文字は含めないでください。\n\n"
            "```json"
            "{"
            "    \"summary\": \"予定の概要\","
            "    \"start\": {"
            "    \"dateTime\": \"YYYY-MM-DD HH:MM\","
            "    },"
            "    \"end\": {"
            "    \"dateTime\": \"YYYY-MM-DD HH:MM\","
            "    }"
            "}"
            "```\n\n"
            "- 日付と時間は24時間表記で「YYYY-MM-DD HH:MM」形式にしてください。\n"
            f"- 現在の日付は{today}です。\n"
            "- 明らかにカレンダー登録とかけ離れる内容の場合は、\"no event found\" と出力してください。\n\n"
            "### 入力テキスト\n"
            f"{received_message}\n\n"
            "### 出力JSON\n"
        )

        # Gemini APIを直接呼び出す
        url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}"
        headers = {
            "Content-Type": "application/json"
        }
        
        # リクエストデータ
        data = {
            "system_instruction": {
                "parts": {
                    "text": system_prompt
                }
            },
            "contents": {
                "parts": {
                    "text": user_prompt
                }
            }
        }
        
        response = requests.post(url, headers=headers, json=data)
        response_json = response.json()
        
        print("Gemini API response:", response_json)  # デバッグ用ログ
        
        # レスポンスからテキスト部分を抽出
        content = response_json.get("candidates", [{}])[0].get("content", {})
        parts = content.get("parts", [{}])
        result_text = parts[0].get("text", "") if parts else ""

        # マークダウン記法(```json と ```)，改行を削除
        result_text = result_text.replace("```json", "").replace("```", "").replace("\n", "").strip()
        
        print("result_text",result_text)  # デバッグ用ログ

        return result_text
    
    except Exception as e:
        error_message = f"予定の追加に失敗しました。エラー：{e}"
        print(error_message)
        return error_message


def add_google_calendar_event(json_text, calendar_id):
    """Googleカレンダーに予定を追加する関数 (OAuth 2.0 サービスアカウント認証を使用)"""

    try:
        # サービスアカウント認証情報を使用してGoogle Calendar APIクライアントを構築
        credentials, project_id = google.auth.default()  # タプルを分解して取り出す
        service = build('calendar', 'v3', credentials=credentials)  # credentials のみを渡す

        # json_textが"no event found"の場合はエラーを発生させる
        if json_text == '"no event found"' or json_text == 'no event found':
            raise Exception("カレンダーに追加したい予定を送ってね！！")
        
        # json_textが文字列の場合、パース
        if isinstance(json_text, str):
            json_text = json.loads(json_text)
            
        JST = datetime.timezone(datetime.timedelta(hours=+9), 'JST')
        summary = json_text["summary"]
        start_time = json_text["start"]["dateTime"]
        end_time = json_text["end"]["dateTime"]

        event_details = {
            'summary': summary,
            'start': {
                'dateTime': datetime.datetime.strptime(start_time, "%Y-%m-%d %H:%M").replace(tzinfo=JST).isoformat(),
                'timeZone': 'Asia/Tokyo', 
            },
            'end': {
                'dateTime': datetime.datetime.strptime(end_time, "%Y-%m-%d %H:%M").replace(tzinfo=JST).isoformat(),
                'timeZone': 'Asia/Tokyo', 
            },
        }

        event = service.events().insert(calendarId=calendar_id, body=event_details).execute() # APIリクエストを実行

        print("Google Calendar event created successfully")
        return True, f"予定を追加しました！！\n 概要：「{summary}」\n時刻：（{start_time}～{end_time}）"

    except Exception as e: # エラー処理を修正
        error_message = f"予定の追加に失敗しました。\nエラー：{e}" # エラーメッセージに例外オブジェクトeを含める
        print(error_message)
        return False, error_message
```
</details>

<details>
<summary>credentials.json（1-3で作成したものをアップロードしてください。値は空白にしています）</summary>

```json
{
    "type": ,
    "project_id": ,
    "private_key_id": ,
    "private_key": ,
    "client_email":,
    "client_id": ,
    "auth_uri": ,
    "token_uri": ,
    "auth_provider_x509_cert_url": ,
    "client_x509_cert_url": ,
    "universe_domain":
  }
```
</details>

### 3-2. 環境変数の追加

Lambda関数の画面から「設定」＞「環境変数」にアクセスし、以下の環境変数を登録します：

- `CHANNEL_ACCESS_TOKEN`：LINEのチャンネルアクセストークン
- `CALENDAR_ID`：GoogleカレンダーのID
- `GOOGLE_APPLICATION_CREDENTIALS`：`/var/task/credentials.json`（credentials.jsonファイルのパス）
- `GEMINI_API_KEY`：GeminiのAPIキー（[Google AI Studio](https://aistudio.google.com/prompts/new_chat?hl=ja)から取得可能）

### 3-3. Lambda関数のレイヤーを追加

Lambda関数で外部ライブラリを使用するには、レイヤーを追加する必要があります。今回必要なライブラリは以下の通りです：

- google-auth
- google-api-python-client
- requests

レイヤーの作成手順：

1. ローカル環境でPython 3.9をインストール(Dockerでやったほうがいいかも）
2. 以下のコマンドを実行してライブラリをインストール：

```bash
mkdir -p python/lib/python3.9/site-packages
pip install google-auth google-api-python-client requests -t python/lib/python3.9/site-packages
zip -r layer.zip python
```

3. 作成した`layer.zip`をLambdaコンソールからレイヤーとしてアップロード
4. Lambda関数の画面から「レイヤー」＞「レイヤーの追加」で作成したレイヤーを選択

## 4. LINE Webhook URLの設定

LINE Official Account Managerに戻り、「Messaging API設定」から「Webhook URL」を設定します。URLはLambda関数の関数URLを入力します。

また、「Webhook送信」を「オン」に設定してください。

## 5. 動作確認

設定が完了したら、LINEアプリから作成したBotに友達登録し、以下のような予定メッセージを送信してみましょう：
```txt
明日の13時から15時まで会議があります
```

Botが予定を解析してGoogleカレンダーに登録し、結果を返信してくれるはずです。


## 参考にしたサイト
https://zenn.dev/yoshida_taro/articles/c872550cd778cb