[GoogleAppsScript] じゃんけんLINE BOTを作成する

# はじめに
LINE を使用して じゃんけんを行うBOTを作成しました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/dd511cef-72e2-387c-fc5d-d3a73df00600.png" width=50%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/cfb3dd2b-328e-624a-02fa-01a806e5f8be.png" width=50%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/40dbc892-84c2-e856-0b55-7af021ac2a60.png" width=50%>

機能としては下記の通りです。

- LINEのじゃんけんチャンネルをフォローする。
- ユーザーがじゃんけんの手を発言すると、相手が応える。テキストとスタンプに対応。
- じゃんけんの勝敗を通算で表示する。

# 事前準備
## チャンネルトークンの発行
[LINE Messaging API](https://developers.line.biz/ja/services/messaging-api/)にて、チャンネルを作成します。チャンネルトークンを発行します。
1. [LINE Developers](https://developers.line.biz/ja/)にログインします。
2. `Create a new channel`を選択します。 
3. `Messaging API`を選択します。
4. 必要な項目を入力して、`Create`でチャンネルを作成します。
5. 作成したチャンネルの`Messaging API`を選択します。
6. `Channel access token`が[GoogleAppsScript（GAS）](https://developers.google.com/apps-script)に設定するトークンになります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/da2edf5a-d4de-588f-3e5e-d657a1c68024.png" width=100%>

## チャンネルアクセストークンの設定
GASにて、WEBサーバーを立ち上げます。
1. Googleにログインします。
2. GoogleDriveにてGoogleAppsScriptを開いて、以下のスクリプトを実装します。

- `LINE_CHANNEL_TOKEN`にLINE Messaging APIで発行したトークンを指定します。
- `SSID`にスプレッドシートのIDと`SSN_USER`にシート名を指定します。

```JavaScript:main.gs
/**
 * じゃんけんBOT
 */
const LINE_CHANNEL_TOKEN = '*****'; // LINE NOTIFYのアクセストークン
const SSID = '*****';
const SSN_USER = 'user'
```

## WEBサーバーの起動
1. GASのメニュー`公開`→`ウェブ アプリケーションとして導入...`
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/028aff81-a934-3447-73fe-8c24476e30dc.png)

2. `Who has access to the app:`で「Anyone, even anonymous」を選択します。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/0fe59c04-a30b-fd70-ec76-da589b938aad.png)

3. `Current web app URL:`がLINEのWebhook URLになります。
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/05296f2f-06b5-0d87-d863-5f0537123992.png)

## Webhookの登録
LINE Messaging APIにて、GASのWEBサーバーURLをWebhookに登録します。
1. LINE Developersにログインして、作成したチャンネルを選択します。
2. `Webhook settings`にWebhook URLを指定します。
![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/23254798-7db5-bdd8-ddb1-089280beae67.png)

3. `Verify`ボタンを実行して、`Success`になることを確認します。
![16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/b4575f84-79a2-29b7-bc9e-b75ad0bb679f.png)

## チャンネルの友達登録
1. LINE Developersにログインして、作成したチャンネルを選択します。
2. `QR code`を読み込みます。

# フォロー登録
ユーザーがチャンネルをフォローすると、`LINE Messaging API`からGASにPOSTが通知されます。GASでは、フォローされたときにユーザー情報をスプレットシートに登録します。

```plantuml
@startuml
actor ユーザー
ユーザー -> LINEアプリ: チャンネルのフォロー
activate LINEアプリ
LINEアプリ -> "LINE Messaging API"
deactivate LINEアプリ
activate "LINE Messaging API"
  "LINE Messaging API" -> GoogleAppsScript: ...POST (type: follow)
deactivate "LINE Messaging API"
  activate GoogleAppsScript
    GoogleAppsScript -> GoogleAppsScript: スプレットシートにユーザー情報の登録
  deactivate GoogleAppsScript
@enduml
```

```JavaScript:main.gs
/**
 * POSTリクエスト
 * @param {Object} event 
 */
function doPost(event) {
    try {
        if (event.postData) {
            let reqObj = JSON.parse(event.postData.contents);
            execute(reqObj);
        }
    } catch (e) {
        console.error(e.stack);
    }
}

/**
 * イベント処理
 * @param {Object} reqObj 
 */
function execute(reqObj) {

    for (let i in reqObj.events) {
        let reqEvent = reqObj.events[i];
        console.log(reqEvent);

        switch (reqEvent.type) {
            case 'follow':
                executeFollow(reqEvent);
                break;
            case 'unfollow':
                executeUnfollow(reqEvent);
                break;
            case 'message':
                executeMessage(reqEvent);
                break;
        }
    }
}

/**
 * Followイベント処理
 * @param {Object} reqEvent 
 */
function executeFollow(reqEvent) {
    let msgList = [{
        'type': 'text',
        'text': 'スタンプまたはメッセージでじゃんけんができます。',
    }];
    sendLinePush(reqEvent.source.userId, msgList);

    let user = getUser(reqEvent.source.userId);
    if (user) {
        userSheet.getRange(user.index + 2, 3).setValue(1);
    } else {
        userSheet.appendRow([reqEvent.source.type, reqEvent.source.userId, 1, 0, 0, 0]);
    }
}

/**
 * ユーザーを取得する
 * @param {String} userId 
 */
function getUser(userId) {
    let userList = getUserList();
    for (let i in userList) {
        let user = userList[i];
        if (user.userId === userId) {
            return {
                index: parseInt(i),
                item: user
            };
        }
    }
    return null;
}

/**
 * ユーザー一覧を取得する
 */
function getUserList() {
    let userList = [];
    let lastRow = userSheet.getLastRow();
    if (1 < lastRow) {
        userList = userSheet.getRange(2, 1, lastRow, 6).getValues();
        userList = userList.map((row) => {
            return {
                type: row[0],
                userId: row[1],
                follow: row[2],
                winNum: row[3],
                loseNum: row[4],
                drawNum: row[5],
            }
        });
    }
    return userList;
}
```
- `execute`関数では、LINE Messaging APIからの通知内容により、処理を振り分けしています。`follow`の場合は、ユーザー情報をスプレッドシートに登録します。既に登録済みの場合は、フォローフラグをONにします。

# フォロー解除
ユーザーがチャンネルのフォローを解除すると、`LINE Messaging API`からGASにPOSTが通知されます。GASでは、フォロー解除されたときにユーザー情報をスプレットシートに更新します。

```plantuml
@startuml
actor ユーザー
ユーザー -> LINEアプリ: チャンネルのフォロー解除
activate LINEアプリ
LINEアプリ -> "LINE Messaging API"
deactivate LINEアプリ
activate "LINE Messaging API"
  "LINE Messaging API" -> GoogleAppsScript: ...POST (type: unfollow)
deactivate "LINE Messaging API"
  activate GoogleAppsScript
    GoogleAppsScript -> GoogleAppsScript: スプレットシートにユーザー情報の更新(unfollow)
  deactivate GoogleAppsScript
@enduml
```

```JavaScript:main.gs
/**
 * UnFollowイベント処理
 * @param {Object} reqEvent 
 */
function executeUnfollow(reqEvent) {
    let user = getUser(reqEvent.source.userId);
    if (user) {
        userSheet.getRange(user.index + 2, 3).setValue(0);
    }
}
```
- `execute`関数では、LINE Messaging APIからの通知内容により、処理を振り分けしています。`unfollow`の場合は、ユーザー情報をスプレッドシートにフォローフラグをOFFに更新します。

# リプライ
ユーザーがメッセージを入力すると`LINE Messaging API`からGASにPOSTが通知されます。GASでは、メッセージ入力されると、その内容を解析して、じゃんけんの結果と通算を出力します。その後、`LINE Messaging API`にPOSTで返します。

```plantuml
@startuml
actor ユーザー
ユーザー -> LINEアプリ: メッセージ入力
activate LINEアプリ
LINEアプリ -> "LINE Messaging API"
deactivate LINEアプリ
activate "LINE Messaging API"
  "LINE Messaging API" -> GoogleAppsScript: ...POST (type: message)
deactivate "LINE Messaging API"
  activate GoogleAppsScript
    GoogleAppsScript -> GoogleAppsScript: メッセージ内容を解析
    GoogleAppsScript -> GoogleAppsScript: じゃんけんの結果, 通算出力
    GoogleAppsScript -> "LINE Messaging API": POST /message/reply
  deactivate GoogleAppsScript
activate "LINE Messaging API"
"LINE Messaging API" -> LINEアプリ
deactivate "LINE Messaging API"
activate LINEアプリ
LINEアプリ -> LINEアプリ: じゃんけん結果を表示
deactivate LINEアプリ

@enduml
```

```JavaScript:main.gs
/**
 * メッセージイベント処理
 * @param {Object} reqEvent 
 */
function executeMessage(reqEvent) {
    let msgList = [];
    let user = getUser(reqEvent.source.userId);
    if (user) {
        let reqHand = -1;
        switch (reqEvent.message.type) {
            case 'text':
                reqHand = getHandFromText(reqEvent.message.text);
                break;
            case 'sticker':
                reqHand = getHandFromSticker(reqEvent.message.packageId, reqEvent.message.stickerId);
                break;
        }

        if (-1 < reqHand) {
            let botHand = getRandom(0, 2);
            let judge = (reqHand - botHand + 3) % 3;

            const BOT_HAND_LIST = ['グー', 'チョキ', 'パー'];
            const RESULT_LIST = ['あいこ', 'あなたの負け', 'あなたの勝ち'];


            let winNum = user.item.winNum;
            let loseNum = user.item.loseNum;
            let drawNum = user.item.drawNum;
            switch (judge) {
                case 0:
                    drawNum++;
                    break;
                case 1:
                    loseNum++;
                    break;
                case 2:
                    winNum++;
                    break;
            }

            msgList.push({
                'type': 'text',
                'text': BOT_HAND_LIST[botHand],
            });
            msgList.push({
                'type': 'text',
                'text': `${RESULT_LIST[judge]}\n\n勝ち: ${winNum}\n負け: ${loseNum}\nあいこ: ${drawNum}`,
            });
            sendLineReply(reqEvent.replyToken, msgList);

            userSheet.getRange(user.index + 2, 4).setValue(winNum);
            userSheet.getRange(user.index + 2, 5).setValue(loseNum);
            userSheet.getRange(user.index + 2, 6).setValue(drawNum);

        } else {
            msgList.push({
                'type': 'text',
                'text': 'わからないよ\nもう一度入力してね',
            });
            sendLineReply(reqEvent.replyToken, msgList);
        }
    }
}

/**
 * テキストからの手を取得する
 * @param {String} text 
 */
function getHandFromText(text) {
    const HAND_LIST = [
        ['グー', 'ぐー', '✊', '👊'],
        ['チョキ', 'ちょき', '✌️', '✌️'],
        ['パー', 'ぱー', '✋', '🖐']
    ];
    for (let i in HAND_LIST) {
        let hand = HAND_LIST[i];
        for (let j in hand) {
            let word = hand[j];
            if (text == word) {
                return parseInt(i);
            }
        }
    }
}

/**
 * スタンプから手を取得する
 * @param {String} packageId 
 * @param {String} stickerId 
 */
function getHandFromSticker(packageId, stickerId) {
    const HAND_LIST = [
        [{
            packageId: '2000010',
            stickerId: '219905'
        }, {
            packageId: '2000000',
            stickerId: '47906'
        }],
        [{
            packageId: '2000010',
            stickerId: '219907'
        }, {
            packageId: '2000000',
            stickerId: '47904'
        }],
        [{
            packageId: '2000010',
            stickerId: '219910'
        }, {
            packageId: '2000000',
            stickerId: '47905'
        }]
    ];
    for (let i in HAND_LIST) {
        let hand = HAND_LIST[i];
        for (let j in hand) {
            let item = hand[j];
            if (item.packageId == packageId && item.stickerId == stickerId) {
                return parseInt(i);
            }
        }
    }
}

function getRandom(min, max) {
    let random = Math.floor(Math.random() * (max + 1 - min)) + min;
    return random;
}

/**
 * LINEに応答メッセージを送信する
 * @param {String} replyToken リプライトークン
 * @param {Object} msgList メッセージリスト
 */
function sendLineReply(replyToken, msgList) {
    let url = 'https://api.line.me/v2/bot/message/reply';
    let options = {
        'method': 'post',
        'headers': {
            'Content-Type': 'application/json; charset=UTF-8',
            'Authorization': `Bearer ${LINE_CHANNEL_TOKEN}`
        },
        'payload': JSON.stringify({
            replyToken: replyToken,
            messages: msgList
        })
    };
    let response = UrlFetchApp.fetch(url, options);
    return JSON.parse(response.getContentText('UTF-8'));
}
```
- `execute`関数では、LINE Messaging APIからの通知内容により、処理を振り分けしています。`message`の場合は、メッセージの内容を解析します。テキストまたはスタンプから、じゃんけんの手を割り出します。じゃんけんの演算を行い、結果と通算を`sendLineReply`関数でLINE Messaging APIにPOSTします。

# 参考リンク
- [TwitterじゃんけんBot作ってみた：すずともBot①](https://qiita.com/SuzuTomo2001/items/41a71e6ca7b4999a895e)
- [Messaging APIとGASを使ったLINE Botでグループチャットの活発化](https://qiita.com/MxShun/items/7a563a795d41cdc0f1dc)

# さいごに
ソースコードをGitHubに公開しています。

- [ソースファイルはこちら](https://github.com/yunbow/GoogleAppsScript-LINE-RPS)

以上です。