LINEDC リプリーグ開催前後 （前=>後）

この記事はLINEDCアドベントカレンダー12/13(月)のものでもあります。

https://qiita.com/advent-calendar/2021/linedc

# 12/13 LINEDCイベントでハンズオン開催

https://linedevelopercommunity.connpass.com/event/231667/

今日(12/13)の夜に開催のイベントですが、果たしてどうなることやらドキドキしながら書いています。
と同時に、開催後にまた記事の更新をかけられたと思っていますので、是非イベントにも参加をポチとお願いします。

LINEDCのYoutubeチャンネルもありますので、こちらもポチとご登録頂けますと嬉しいです。

https://www.youtube.com/channel/UCZkYYwmvSA6y7WWLxM5x9IA/featured

# （更新）イベント後

無事には終わりませんでした・・笑
ご興味ある方は是非イベントの動画アーカイブをご覧ください。

https://youtu.be/9k5tt1n0ai0

# こちらでもリプリーグの様子をご覧頂けます！

https://youtu.be/g6zgTWDi7l8?t=4472

# こんな感じです

https://twitter.com/h2m_kinoko/status/1474566908553101313


# サーバーいらずのGAS

GASの魅力は業務効率化・自動化もさることながら、サーバーを別途用意せずともLINE Botなどを作成できるところです。
Googleスプレッドシートとの連携はよく見かけますが、Goolgeスライドを使って何かやりたいなーと思っていたところで、今回のリプリーグネタが思い浮かびました。まさに月曜日の夜のテレビ番組でやっているアレですね。奇しくも今日は月曜日。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119864/f33bd164-fa53-cecc-ad97-8eab051e7a44.png)

# リプリーグって？

最大10人（2チームx5人）で対戦できるクイズゲームです。
身内＆オンラインでスライドを画面共有しながら遊ぶことを前提にしています。
オンラインでのイベントにいかがでしょ？？
ざっくりした流れは以下の通りですが、実際の使い方は、イベントにご参加いただくが、Youtubeアーカイブをご覧ください!

1. 出題者を設定後、参加人数を決める
2. 参加者を募集
3. 参加者エントリー
4. エントリー完了後、問題と解答をセットし、出題
5. 参加者は自身が担当する箇所の解答（ひらがな1文字）を送信
6. 結果オープン

画面も何となくこんな感じというのを貼っておきます。

#### 出題者設定

「出題者」とメッセージを送ることで、当該ユーザーが出題者に設定されます。
出題者にはリッチメニューが表示されます。（このあたり、Messaging APIで実現しているところです）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119864/654d84e3-d907-e233-8b8a-e74517b5f069.png)

#### リプリーグ開始

「リプリーグ開始」すると、リーグIDが発行されます。（参加者はこのリーグIDを送信することで参加）
チーム数と1チームあたりの参加人数を送信し、準備完了です。

![IMG_7F675B64D69A-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119864/ac2d4e56-918f-a0ff-2655-dd3a7cb9dd6d.jpeg)

#### 解答オープン

Googleスライドにて。某番組でみかけるような雰囲気で正解を表示してます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119864/6b4b3606-bd35-dc44-8027-9700ffa5daa0.png)


以降、少しだけ技術寄りの内容に触れたいと思いますので、初めての方の参考になりましたら。

# Messaging API


応答メッセージとプッシュメッセージがあります。Messaging APIを使うことで、ボットサーバー（ここではGAS)とLINEプラットフォームの間でデータのやり取りができます。

https://developers.line.biz/ja/docs/messaging-api/overview/

>Messaging APIを使って、ユーザー個人に合わせた体験をLINE上で提供するボットを作成できます。
>作成したボットは、LINEプラットフォームのチャネルに紐づけます。チャネルを作成すると生成されるLINE公式アカウントをボットモードで運用すると、LINE公式アカウントがボットとして動作します。

>ユーザーが、LINE公式アカウントにメッセージを送信します。
>LINEプラットフォームからボットサーバーのWebhook URLに、Webhookイベントが送信されます。
>Webhookイベントに応じて、ボットサーバーからユーザーにLINEプラットフォームを介して応答します。

「LINEプラットフォームからボットサーバーのWebhook URL」とあるのがGASのデプロイURLです。

この設定のおかげで、LINEでメッセージを送信するとGASで受信できるということですね。

# GASで受信する

https://developers.line.biz/ja/docs/messaging-api/receiving-messages/

>ユーザーが、LINE公式アカウントを友だち追加したり、LINE公式アカウントにメッセージを送ったりすると、LINE Developersコンソールの「Webhook URL」に指定したURL（ボットサーバー）に対して、LINEプラットフォームからWebhookイベントオブジェクトを含むHTTP POSTリクエストが送られます。

ふむふむ。「POSTリクエスト」とあります。GASでPOSTリクエストを受けている箇所が以下になります。

```main.gs
function doPost(request) {
}
```

引数の`request`にはどんなパラメーターが入ってくるかは、以下で確認ができますね（全然関係ありませんが、Chromeだと該当箇所へのリンクを生成できるようになりましたね）

- Webhookイベントオブジェクト

https://developers.line.biz/ja/reference/messaging-api/#:~:text=%23-,Webhook%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88,-LINE%E3%83%97%E3%83%A9%E3%83%83%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%81%A7


```
"destination": "xxxxxxxxxx",
"events": []
```



`destination`と`events`があります。`events`は配列になってますが、「1つのWebhookに複数のWebhookイベントオブジェクトが含まれる場合があります」ようです。
リプリーグでは以下の様にJSON形式に変換してから1つ目のイベント情報を取得しています。

```main.gs
const receiveJSON = JSON.parse(request.postData.contents);
const event = receiveJSON.events[0];
```

あとは、`event.type`や`event.message`などを頼りに処理していきます。


# LINEに応答を返す

リプリーグでは、「出題者」と「回答者」で処理を分けていますが、例えば「出題者」と送信したら、「出題者として設定されました。」と応答が返ってきます。これが「応答メッセージ」になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119864/654d84e3-d907-e233-8b8a-e74517b5f069.png)

- 応答メッセージを送る

https://developers.line.biz/ja/reference/messaging-api/#send-reply-message

>ユーザー、グループ、またはトークルームからのイベントに対して応答メッセージを送信するAPIです。
>応答メッセージを送るには、Webhookイベントオブジェクトに含まれる応答トークンが必要です。

とあります。

>POST https://api.line.me/v2/bot/message/reply

上記の通りGASからPOSTメッセージを送信する必要があります。以下が該当箇所になってます。


```line-api.gs
replyToUser(replyToken, replyMessages) {
    const messages = [];
    replyMessages.forEach((m) => {
      messages.push({
        type: 'text',
        text: m
      })
    })

    const replyText = {
      replyToken: replyToken,
      messages: messages
    };

    const options = {
      method: 'post',
      headers: {
        'Content-Type': 'application/json',
        Authorization: 'Bearer ' + CHANNEL_ACCESS_TOKEN,
      },
      payload: JSON.stringify(replyText),
    };


    UrlFetchApp.fetch(LINE_API_REPLY, options);
}
```

`method: 'post'`とあるように`post`メッセージを`UrlFetchApp.fetch`にて送信しています。

ここで必要になるのが、 「チャネルアクセストークン」と「応答トークン」です。
「チャネルアクセストークン」は、Messaging APIのコンソールで発行したあの長い文字列ですね。イベント内では、チャネルアクセストークンをスプレッドシートに記述し、以下の箇所にてスプレッドシートから取得しています。

```line-api.gs
const CHANNEL_ACCESS_TOKEN = Sheet.Admin.getRange('Token').getValue();
```

# メッセージをプッシュする

リプリーグでは、応答メッセージのほか、例えば以下のようなタイミングでプッシュメッセージを使用しています。
・出題者がリプリーグで参加者を募集したタイミングで、登録されている友達にメッセージを送る
・参加者が全員揃ったタイミングで出題者にメッセージを送る

LINEからメッセージを送ってその応答を返すのではなく、何かのトリガー（タイミング）を契機にメッセージをユーザーに通知するイメージですね。

- プッシュメッセージ

https://developers.line.biz/ja/docs/messaging-api/sending-messages/

> POST https://api.line.me/v2/bot/message/multicast

プッシュメッセージには、以下の４種類がありますが、リプリーグでは「マルチキャストメッセージ」という形でユーザーIDを指定して送信しています。

・　プッシュメッセージ（1対1）
・　マルチキャストメッセージ（1対多：ユーザーID指定）
・　ナローキャストメッセージ（1対多：絞り込み配信）
・　ブロードキャストメッセージ（1対多：すべての友だち）

```line-api.gs
multicastToUsers(message, targetUserIdList) {
    const postData = {
      to: targetUserIdList,
      messages: [
        {
          type: 'text',
          text: message,
        },
      ],
    };

    const headers = {
      'Content-Type': 'application/json; charset=UTF-8',
      Authorization: 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    };

    const options = {
      method: 'POST',
      headers: headers,
      payload: JSON.stringify(postData),
    };

    UrlFetchApp.fetch(
      LINE_API_MULTICAST,
      options
    );
}
```


# 「出題者」にリッチメニューを表示

リプリーグでは、Messaging APIを使い「出題者」に設定されたユーザーに対してリッチメニューを紐づけています。手順は以下の通りです。

1.　リッチメニューの作成（一意のリッチメニューIDを取得できる）
2.　リッチメニューIDに対してリッチメニュー用の画像を登録
　　（リプリーグでは、2500px x 1686px の６区画固定になってます。）
3.　リッチメニューIDに対してユーザー（ユーザーID)を紐づける

- リッチメニュー

https://developers.line.biz/ja/reference/messaging-api/#rich-menu

```line-api.gs
  // 1.　リッチメニューの作成（一意のリッチメニューIDを取得できる）
  createRichMenu() {

    const postData = {
      "size": {
        "width": 2500,
        "height": 1686
      },
      "selected": false,
      "name": "リプリーグ管理者用",
      "chatBarText": "リプリーグ",
      "areas": [
　　　　　　　　　　　　　　　　　//(略)
        // リッチメニューの区画に合わせて座標とアクション等を指定
      ]
    };


    const headers = {
      'Content-Type': 'application/json; charset=UTF-8',
      Authorization: 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    };

    const options = {
      method: 'POST',
      headers: headers,
      payload: JSON.stringify(postData)
    };

    const response = UrlFetchApp.fetch(
      LINE_API_RICHMENU,
      options
    );

    return JSON.parse(response.getContentText());
  }

  // 2.　リッチメニューIDに対してリッチメニュー用の画像を登録
  uploadImageForRichMenu(id, img) {
    const headers = {
      'Content-Type': 'image/jpeg',
      Authorization: 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    };

    const options = {
      method: 'POST',
      headers: headers,
      payload: img
    };

    UrlFetchApp.fetch(
      `${LINE_API_UPLOAD_RICHMENU_IMG}/${id}/content`,
      options
    );
  }

  // 3.　リッチメニューIDに対してユーザー（ユーザーID)を紐づける
  attachRichMenuToUser(userId, richMenuId) {
    const headers = {
      'Content-Type': 'image/jpeg',
      Authorization: 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    };

    const options = {
      method: 'POST',
      headers: headers
    };

    UrlFetchApp.fetch(
      `${LINE_API_ATTACH_RICHMENU_USER}/${userId}/richmenu/${richMenuId}`,
      options
    );
  }
```

また、「出題者」が変更された場合、変更前の出題者のリッチメニューを削除しています。

```line-api.gs
  detachRichMenuFromUser(userId) {
    const headers = {
      Authorization: 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    };

    const options = {
      method: 'DELETE',
      headers: headers
    };

    return UrlFetchApp.fetch(
      `${LINE_API_ATTACH_RICHMENU_USER}/${userId}/richmenu`,
      options
    );
  }
```

# おしまい

GASを使ってのスプレッドシートの操作やスライドの操作については触れてませんが、是非Youtube等をご視聴ください!!
メリークリスマス♪