アイマスライブのチケット申し込みを忘れてしまう人のためにLINEBotを作ってみた 

この記事は「[PERSOL PROCESS & TECHNOLOGY Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ppt)」の17日目の記事です。
他にも諸先輩方、同期、後輩の皆さんが素敵な記事を寄せていますので、よろしければご覧になってください。

# はじめに

こんにちは。
Kent Moritaです。
パーソルプロセス＆テクノロジーという会社でシステム開発を行っています。
お仕事ではOSSチャットフレームワークを利用したグループ向けチャットシステム2種の開発・保守・運用をしています。
そのうち一種は、LINE Messaging APIを利用して派遣スタッフがLINEからコーディネーター（お仕事の紹介などをする人たち）とコミュニケーションを取れるようにするためのシステムです。

プライベートでは__THE IDOLM@STERシリーズ__が好きで、音楽を聞いたりゲームをしたり、ライブに行ったりしています。
ライブに参加することは好きなのですが、「ご用意できませんでした」（チケットの抽選に外れること）以前に__申し込みを忘れることで枕を血で染めることが多く発生したため__、対策の第一歩として自分でいい感じに情報を取得できるものを作ってみようと思いたち、実施してみました。

※この仕組みは2020年3月ごろに作成したものです。
COVID-19によってアイマスに限らず、あらゆるライブコンテンツはそのあり方を大きく変えざるを得ない状況となってしまいましたが、
アイマスにおいては運営の皆さん、及びプロデューサーの皆さんの尽力により、（大きく形が変わったとはいえ）現地に参加する形式のライブを開催することができています。
この仕組み自体は現在も使えるため、（今後ブラッシュアップするためのモチベーション維持のためにも）ここに記載します。

# 概要

簡単な操作で__アソビストアのアイマス関連のライブ情報を取得できるLINE Botを作成しました。__
ユーザーがLINEのリッチメニューから取得したいコンテンツを選択したり、テキストでメッセージを送ったりした場合、LINE Messaging APIからHEROKU上のアプリにリクエストを行います。
HEROKU上のアプリケーションでは、リクエストを受け取るとアソビストアのサイトに対してWebスクレイピングを行い、選択したコンテンツの先行申込情報をLINEユーザーに返却します。
__なお、完成したLINE Botは以下のQRコードより友達追加することが可能です。__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409466/2c784285-d9c0-6074-8919-cdbdd2f6eea1.png)
また、HEROKU上で動いているアプリケーションについては[GitHub上で公開しています。](https://github.com/Kent-Morita/get-imas-live-applivation-information)

# なぜ作ろうと思ったのか

- アイドルマスターシリーズのライブへは「CD封入先行」「ゲーム先行」「WEB先行」「一般申込み」「機材開放」などの方法があります。
- 2018年7月より__「アソビストアプレミアム会員最速先行」__という申込方法が新しく増えました（2年ほど経ちますが、かなり当たる印象があります）
- ただ、ゲームやCDの発売情報などと連動していない（ケースが多い）ので、__往々にして申込みを忘れがち__です。（私の頭の出来が悪い可能性は大いにありますが…）
- アソビストアの公式サイトで先行申し込みの状況は確認することができますが、他のコンテンツの公演の情報等もあり、視認性があまり良くない…と個人的には考えています。
- なるべく精神的コストの低い形でライブ情報を取得したい、そして同じ悩みを持つプロデューサーの皆さんに共有したいという考えのもと、仕組みを作ってみました。
- また、業務でLINE Messaging APIを使用するため、その使い方について習熟したいという意図もありました。

# 環境
- npm 6.14.3
- Node.js 12.16.1
- LINE Messaging API
- Heroku

# 開発の流れ
以下の手順で作成しました。

- Bot用のLINEのチャネル(種類はMessaging API)を作成する
- アプリの雛形を用意する
- アプリケーションを動かす場所を用意する(今回はHeroku)
- Try & Error で頑張る

# LINEの設定をする
## LINEのチャネルを作成する
インターフェースとしてLINEを使用するため、まずはBot用のLINEチャネルを作成します。
[LINE Developers](https://developers.line.biz/console/)を開き、新規のチャネルを作成します。
チャネルの種類は「Messaging API」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409466/92df3b26-ae14-f49e-fa10-bdaf1e0744b1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409466/e92f4f34-54cd-01e6-74df-256be7e20142.png)

## Messaging APIの設定

チャネル作成時に表示されるチャネルID,チャネルシークレット、チャネルアクセストークンは後ほど使うため、メモをしておいてください。
また、Webhook設定で、後ほど作成するHerokuアプリのURLを入力しておきます。（まだ作成していない場合は後で入力します。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409466/d55bf4e8-e433-aaad-b51b-97e6f01a500e.png)

## リッチメニューの設定をする

チャネルの作成が完了したら、[LINE Official Account Manager](https://manager.line.biz/) (先ほどとは別のサイトです)からリッチメニューの作成を行います。

それぞれの領域をタップすると、「AS」「デレ」「ミリ」「シャニ」「SideM」「指定なし」というテキストを送るように設定します。
※各コンテンツのアイコンについては[Twitterの@device1020さんよりお借りしました。](https://twitter.com/device1020/status/862548892566511617)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409466/576548ac-1aea-ff20-f702-e7cc863dcf2f.png)

# アプリをつくる

## HEROKUについて

- [Heroku](https://jp.heroku.com/)：PaaSの1つ　クレジットカードの登録なしで使える　安い
    - というか今回の構成ならタダです。貧乏人の味方
    - DB,cronなどを使う際はクレジットの登録が必要（有料ではないはず）
- Herokuに登録しHeroku CLIをインストールすれば、heroku login -> heroku createで簡単にアプリケーション動作環境を作成することができる

## 雛形の作成

まずは必要なものをインストールし、アプリの雛形を作成していきます。

- ローカル上でアプリ用のディレクトリを作成する
- Package.jsonのscriptsに「index.js」を追記する
- Line公式から「line-bot-sdk-nodejs」というライブラリが出ていますが、今回は[別のライブラリ](https://www.npmjs.com/package/linebot)を使用しています
- cheerio-httpcliは静的ページのスクレイピングのためのライブラリです

## ディレクトリ構成

ディレクトリ構成は以下のとおりです。

┠ node_modules/ 
┠ routes/ 
┃ ┗ lineBot.js 
┠ index.js 
┠ package.json 
┗ package-lock.json

## ルーティング

index.jsに以下のように記載していきます。

```javascript
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const lineBot = require('./routes/lineBot');

app.set('port', (process.env.PORT || 8000));
app.use(bodyParser.urlencoded({
    extended: true
}));
app.use(bodyParser.json({
    verify(req, res, buf) {
        req.rawBody = buf
    }
}));
app.use(bodyParser.urlencoded({ extended: false }));

app.use('/',lineBot);

app.listen(app.get('port'), function() {
    console.log('Node app is running');
});
```

## LineBot.js

### やること

- メッセージイベントを含む、イベントごとに対する処理を書く
- メッセージを受け取った場合、来たメッセージによって、スクレイピングする処理に渡す値を変える
- スクレイピングするためのfunctionを書く

### イベントごとの処理


Webhookで飛んでくるイベントの種類によって処理を分けます。
（今回は友達追加とブロックイベントを書いてみました）

```javascript
// 友達追加
bot.on('follow', (event) => {
    console.log('follow event');
});
// ブロック
bot.on('unfollow', (event) => {
    console.log('unfollow event');
});
//メッセージイベント
bot.on('message', async (event) => { 
    //処理
});
```

### メッセージイベントを書く

タップした場所に応じて、スクレイピングするfunctionに渡す値を変えるように書きます。
先程、「//処理」とした部分です。

```javascript
//メッセージイベント
bot.on('message', async (event) => {
    console.log('message event');
    if(event.message.type !== 'text') {
        return;
    }
    let replyMessages = [];
    try 
    {
        if(event.message.text.indexOf('AS') !== -1) {
            replyMessages = await getTicketInformation("765AllStars");
        }else if(event.message.text.indexOf('デレ') !== -1){
            replyMessages = await getTicketInformation("CINDERELLA GIRLS");
        }else if(event.message.text.indexOf('ミリ') !== -1){
            replyMessages = await getTicketInformation("MILLION LIVE");
        }else if(event.message.text.indexOf('シャニ') !== -1){
            replyMessages = await getTicketInformation("SHINY COLORS");
        }else if(event.message.text.indexOf('SideM') !== -1){
            replyMessages = await getTicketInformation("SideM");
        }else if(event.message.text.indexOf('指定なし') !== -1){
            replyMessages = await getTicketInformation("THE IDOLM@STER");
        }
        else {
            replyMessages = ["先行申込情報を知りたいコンテンツを選んでください"];
        }
    }catch(err)
    {
        console.log('an error occured!');
        console.log(err);
    }

    event.reply(replyMessages);
});

```

### スクレイピングする処理を書く

Fetchしてきたパース済みHTMLから必要な要素だけ取り出して配列に加えています。
（複数公演があった時、複数メッセージに分けるため配列にしています。）

```javascript
// アソビストアからスクレイピングしてくる関数
async function getTicketInformation(name) {
    const fetchResult = await cheerio.fetch('https://asobistore.jp/event-ticket/List');
    let replyMessages = [];

    // fetchした内容のうち、'table'の中身を一個ずつ取り出してreplyMessageに追加する
    fetchResult.$('table').each(function (index) 
    {
        let replyMessage = '';
        const  table = fetchResult.$(this);
        
        // 引数が含まれていなかったら飛ばす
        if (table.text().includes(name))
        {
            table.find('tr').each(function(index)
            {
                const tr = fetchResult.$(this);
                const th = tr.find('th');
                const td = tr.find('td');
                
                // 表示したいtrだけ表示する（
                if (th.text().includes('公演名')　|| th.text().includes('公演日時') 
                || th.text().includes('チケット情報'))
                {
                    replyMessage += ` ■${th.text()} : ${td.text()} \n`
                    replyMessage += '\n';

                }
                
            });
            replyMessages.push(replyMessage);
            console.log(replyMessages.length);
        }

    });
    if (replyMessages.length === 0)
    {
        replyMessages.push("先行申込情報が見つかりませんでした。シリーズの指定なしで試してみてください。");
    }

    return replyMessages;
}
```

## アプリをHeroku上にデプロイする

まずはherokuのアプリを作成し、変更を直接pushしてみます。

```shell
heroku create
git push heroku master
```

もしくは、githubにリモートリポジトリを作成し、Herok Flowを使ってgithubからHerokuへデプロイするようにします。
手順については[こちらの記事](https://qiita.com/sho7650/items/ebd87c5dc2c4c7abb8f0#github-%E3%81%A8%E9%80%A3%E6%90%BA%E3%81%97%E3%82%88%E3%81%86)に丁寧にまとめられていますので、ご参照ください。


# ここまですると、こうなる
LINE上からリッチメニューの画像を選択すると、公演情報が返ってくるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409466/6d9ca29d-6862-1565-9d8f-dfa3415bb277.png)

# 終わりに
## 今回の反省点と次回の展望
3月ごろにこのアプリを作成し、しばらく触ってみたところ、以下のような反省点が浮かび上がってきました。

- LINEのメッセージから、直接チケット申し込みのページ（今回スクレイピングしたサイトとは別のサイト）に飛びたい
    - スクレイピング後、選択する要素を変える必要はあるが、十分できそう
- そもそもリマインダーの側面が強いので、新規に公演情報が追加されたときはプッシュで教えてほしい
    - HerokuのAdd-onsのcronを使えばできるかもしれない
    - LINE Messaging APIのpushメッセージも1000通以上は有料
        - うっかり破産しそう…
- 「チケット情報」の欄がごちゃごちゃしているので、綺麗にしたい

特に、チケット情報の取得がユーザーの脳みそキックなのはかなり大きな問題（私の脳みそcronの出来が悪すぎるので…）なので、TwitterのBot等を使用して、なんとか自動的に情報が通知されるようにしたいところです。

## Webスクレイピングについて
Webスクレイピングについては違法性がないと認識していますが、使い方によっては偽計業務妨害等で逮捕される可能性があります。
今回のケースでは

- スクレイピング対象のコンテンツが著作物ではない
- アソビストアの利用規約にはスクレイピングを禁止する明示的な記載はない（不正アクセス行為を禁止する旨の記載は存在しています）
- このアプリケーションでは、1回の処理（リッチメニューのタップ）につき、1度のリクエストのみを行っており、このアプリケーションによって行われるスクレイピングは自然検索の範囲内に収まると考えている


上記の理由により問題ないと判断しておりますが、万一上記認識に問題がある場合はご指摘いただけますと幸いです。
参考：https://topcourt-law.com/internet_security/scraping-illegal