筋トレの予定をとあるメンズコーチにコーチングしてもらう

# この記事は何？
Google Calendarの予定をMessagingAPIを使用して、とあるメンズコーチ風に通知する方法を記した記事。

## 概要
最近筋トレさぼってない？厳しいって。
LINEグループに招待して、メンズコーチをメンションすると、GoogleCalendarの予定からグループのメンバーにご指導をいただけるよ。
![ジョージBOTサンプル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3086181/1fe65c14-c76d-862f-3758-8c2442527b3b.png)


## Google Calendar から予定を取得する
1. GCPのコンソールに移動し、Google CalendarのAPIを有効化
2. 自分のカレンダーに移動し、1で作成したbotのメールアドレスにカレンダーを共有

詳しくは以下の記事を参照。
https://www.coppla-note.net/posts/tutorial/google-calendar-api/

Google Calendar から予定を取得し、ジムの回数を取得する関数は以下。
```python
import datetime
import pytz
from googleapiclient.discovery import build
from google.auth import load_credentials_from_file

# 認証情報ファイルのパス
CREDENTIALS_PATH = '../credentials_service.json'
TOKEN_PATH = '../token.json'

# 認証範囲
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']

def get_schedule(calendar_id):
    creds = load_credentials_from_file(
      CREDENTIALS_PATH, SCOPES
    )[0]

    service = build('calendar', 'v3', credentials=creds)
    JST = pytz.timezone('Asia/Tokyo')
    # プログラム実行時間から1週間後までに期間を設定
    now = datetime.datetime.now(JST)
    start_time = now.isoformat()
    end_time = (now + datetime.timedelta(days=7)).isoformat() 
    events_result = service.events().list(calendarId=calendar_id, timeMin=start_time, timeMax=end_time,
                                          maxResults=50, singleEvents=True,
                                          orderBy='startTime').execute()
    events = events_result.get('items', [])
    if not events:
        return None
    
    schedule = []
    for event in events:
        event_info = {
            'summary': event.get('summary'),
            'start': event['start'].get('dateTime', event['start'].get('date')),
            'attendees': event.get('attendees', [])
        }
        schedule.append(event_info)
    return schedule

def count_workout_events(schedule):
    workout_count = {}
    for event in schedule:
        if 'ジム' in event['summary']:
            for attendee in event['attendees']:
                email = attendee.get('email')
                response_status = attendee.get('responseStatus')
                if email and response_status == 'accepted':
                    if email in workout_count:
                        workout_count[email] += 1
                    else:
                        workout_count[email] = 1
    return workout_count

if __name__ == '__main__':
    schedule = get_schedule()
    count = count_workout_events(schedule)
    print(count)
```

## Messaging API によるBOTの作成
### LINE BOT の設定
LINE Deveropersに登録し、コンソールにてプロバイダを作成する。
プロフィール情報を入力し、トークンなどの認証情報を取得する。
取得した認証情報は環境変数に書き込む or 指定したパス上に配置する。

### webhook の設定
以下のタイミングでBOTがアクションを取れるように設定する。
1. グループに参加した時
1. ユーザーにメンションされた時

#### 1. グループに参加した時
BOTがグループに招待された時、グループIDをcsvに保存する。
また、挨拶メッセージを送るようにする。
以下はapp.pyに記述する。
```python 
from flask import Flask, request, abort, jsonify
from linebot import WebhookHandler, LineBotApi
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage, JoinEvent
from dotenv import load_dotenv
import requests
import os
import csv
from main import main

app = Flask(__name__)

load_dotenv()
CHANNEL_SECRET = os.getenv('CHANNEL_SECRET')
CHANNEL_ACCESS_TOKEN = os.getenv('CHANNEL_ACCESS_TOKEN')

if not CHANNEL_SECRET or not CHANNEL_ACCESS_TOKEN:
    raise ValueError("CHANNEL_SECRET and CHANNEL_ACCESS_TOKEN must be set in environment variables.")

handler = WebhookHandler(CHANNEL_SECRET)
line_bot_api = LineBotApi(CHANNEL_ACCESS_TOKEN)

@handler.add(JoinEvent)
def handle_join(event):
    group_id = event.source.group_id
    add_group_id_to_csv(group_id)
    line_bot_api.push_message(group_id, TextSendMessage(text="厳しいって"))

def add_group_id_to_csv(group_id):
    csv_file_path = '../data/group_id.csv'
    file_exists = os.path.isfile(csv_file_path)
    
    with open(csv_file_path, mode='a', newline='') as csv_file:
        writer = csv.writer(csv_file)
        if not file_exists:
            writer.writerow(['group_id'])
        writer.writerow([group_id])
    return  
```

#### 2. メンションされた時
メンションされた時に、main.py のmain関数を実行するようにする。(main.pyは後述)
ユーザーIDは送りたいグループIDを指定する。
グループIDはグループに参加した際csvに保存されるので、csvを読み込む必要がある。
```python 
def load_group_ids(file_path):
    """ ファイルからグループIDを読み込む """
    with open(file_path, 'r') as file:
        return [line.strip() for line in file]
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    user_ids = load_group_ids('../data/group_id.csv')
    if event.message.text == '@ジョージ' and event.source.group_id == user_ids[1]:
        main()
```

### APIの作成
メッセージを送るため、main関数を実行するための処理をapp.pyに書いていく。
```python

def send_push_message(token, user_id, message_text):
    url = 'https://api.line.me/v2/bot/message/push'
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {token}'
    }
    data = {
        'to': user_id,
        'messages': [{
            'type': 'text',
            'text': message_text
        }]
    }
    response = requests.post(url, headers=headers, json=data)
    return response.status_code, response.text

@app.route("/send_message")
def send_message():
    token =  os.getenv('CHANNEL_ACCESS_TOKEN') 
    user_ids = load_group_ids('../data/group_id.csv')
    message_text = request.args.get('message', 'こんにちは、これはプッシュメッセージです！') 
    results = []
    for user_id in user_ids:
        status_code, response_text = send_push_message(token, user_id, message_text)
        results.append({'user_id': user_id, 'Status Code': status_code, 'Response': response_text})
    return jsonify(results)
```

### ngrok の使用方法
webhookを使用するために、ローカルのFlaskサーバーのポートを外部に公開する必要がある。
そのためにngrokを用いた。
```python
if __name__ == '__main__':
    app.run(port=8080)
```
8080番ポートを外部に共有するためには、以下コマンドを実行。
```bash
ngrok http 8080
```
上記のコマンドを打つと、
```bash
Forwarding                    https://hogehoge.ngrok-free.app
```
のように表示されるので、リンクをコピー。

MessagingAPIの設定ページに移動し、webhookURLを設定。

https://hogehoge.ngrok-free.app/callback と入力。

これにて設定完了。
BOTをグループに招待し、メンションすると、カレンダーの予定をもとにご指導をいただける形となった。

## 実装できなかったこと
1. グループのユーザーをメンションすること
    LINE Developersの有料会員にならないとグループにいるユーザーのユーザーIDを取得することができなかったため
1. メンションイベントをwebhookとして受け取ること
    そもそも存在しなかった。Slack等ではできるらしい
1. ユーザーごとに予定を取得
    本来であれば招待されたグループごとにカレンダーのIDを参照できるのがのぞましい。今回は個人利用のため、自分のカレンダーのみを取得し、グループメンバーには筋トレに行く時には自分を招待してもらうようにした。

## コード
https://github.com/ikuoikuo/calendar_bot