徐々に壊れていくLINE BOTを作ってみた

# 目次
1. このLINE BOTのコンセプト
1. 使用した技術
1. 手順アウトライン
1. 手順詳細
1. 完成したもの(画像)
1. お友達に?
1. github
1. イラストについて

## このLINE BOTのコンセプト
 - 我々の知らないところで終わりそうな世界がある。
 - その世界でかろうじて機能しているBOTがいた。
 - BOTはあてもなく通信を試みてたらあなたとつながった。
 - BOTは終わりそうな世界の話をするよりもあなたとたわいもない雑談をしながら最後を迎えることにした。
 - モニターがあってBOTの顔を見ることができる(ただの画像ね)。
 - 徐々に壊れていく様子が返事やモニターを通してわかる。

作ろうと思ったときに思いついたのはこれくらい。クオリティは気にしちゃだめ。

## 使用した技術
- node.js (v8.10.0)
   - Express(v4.16.4) - node.jsのフレームワークを利用するためのパッケージ
   - @line/bot-sdk (v6.3.0) - LINEのMessaging APIを利用するためのパッケージ
   - request (v2.88.0) - a3rtのTalk APIへPOSTするためのパッケージ
- now.sh (v12.1.3) - 最終的にできたものをここにデプロイする
- ngrok (v2.2.8) - テストするときにいちいちnow.shにデプロイするの面倒くさいからローカル環境に直接繋げられるツール

## 手順アウトライン
階層構造をもっとうまく表したかったけど、これで我慢…

1. 開発前の事前準備
   1. (1-1) LINE BOTのアカウント作成 & 設定
   1. (1-2) a3rtのTalk APIのキーの取得
2. node.jsでBOT開発
   1. (2-1) プロジェクトの土台を作成
   1. (2-2) LINE BOTにメッセージを送れるようにする
   1. (2-3) Talk APIにリクエストを送って返事をもらえるようにする
   1. (2-4) 壊れている様子を文字で演出する
   1. (2-5) "モニター"機能の追加
   1. (2-6) 壊れている様子を"モニター"で演出する
   1. (2-7) 最後のディテールをちょいちょい
3. now.shでネットにデプロイ
   1. (3-1) デプロイ前にすること
   1. (3-2) デプロイ


## 手順詳細
#### 1. 開発前の事前準備
開発を始める前にアカウント登録やAPIキーの取得などをしないといけない! ここらに関してはネットに資料がたくさん転がっているので自分が参照した資料を記載しておく。

###### (1-1) LINE BOTのアカウント作成 & 設定

- LINE BOTのアカウント作成だけでなく、now.sh、ngrok、そしてLINE BOTのベースとなるコードも教えてくれる。
    - [1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d "1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest")

- LINE BOTのアカウン作成とherokuへのデプロイ。私はherokuではなく、ngrok/now.shを使ったので、どちらを使うかはお任せ。
    - [LINEのBot開発 超入門（前編） ゼロから応答ができるまで](https://qiita.com/nkjm/items/38808bbc97d6927837cd "LINEのBot開発 超入門（前編） ゼロから応答ができるまで")

###### (1-2) a3rtのTalk APIのキーの取得

- 以下のリンクで跳んだ先に「API KEY 発行」ボタンがあるのでクリック。その後のページでメアドを登録するとメールでAPI KEYを送ってくれる。後のステップでAPI KEYを用いるのでメモメモ…
    - https://a3rt.recruit-tech.co.jp/product/talkAPI/

#### 2. node.jsでBOT開発

###### (2-1) プロジェクトの土台を作成

- プロジェクトディレクトリの作成とnpm init -y
- 必要なパッケージのインストール
- `server.js`ファイルを作成

```linux:ターミナル
$ mkdir <プロジェクト名>
$ cd <プロジェクト名> 
$ npm init -y
$ npm i --save express @line/bot-sdk request  # バージョン気になる人はバージョンも
$ npm i --save-dev ngrok now
$ touch server.js
```


###### (2-2) LINE BOTにメッセージを送れるようにする

- LINE BOTのチャンネル設定ページの「Channel Secret」と「Channel access token」を発行/メモ
- LINE BOTのチャンネル設定ページの「Use webhooks」を「Enabled」に設定
- LINE BOTと会話するための最低限のコードを記載

```JavaScript:server.js
// パッケージのインポート
const express = require('express');
const line    = require('@line/bot-sdk');


// LINE BOTのチャンネル設定ページの「Channel Secret」と「Channel access token」の値
const lineConfig = {
    channelSecret:      'XXXXXXXXXXXXXXXXXXXX',
    channelAccessToken: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
};


// Expressの起動
const portNum = process.env.PORT || 3000; // 環境変数に決められたポートあればそれを、なければ3000
const app = express();
app.listen(portNum, () => {
    console.log(`Server running at ${portNum}`);
});


// LINE BOTのwebhookのためのルーティング
app.post('/webhook', line.middleware(lineConfig), (req, res) => {
    console.log(req.body.events);
    Promise
      .all(req.body.events.map(handleEvent)) // このhandleEventの中身はちょっと下に定義してあるfunctionだよ
      .then((result) => res.json(result));
});


// これがなにしてるかちょっとわからないからエロい人教えて
const client = new line.Client(lineConfig);


// LINE BOTがユーザーから受け取ったイベントからいろいろ処理をするイベントハンドラ
// LINE BOTの返信に関してのロジックは全部ここ
function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  return client.replyMessage(event.replyToken, {
    type: 'text',
    text: event.message.text // 実際に返信の言葉を入れる箇所(event.message.textはユーザーが送ったテキスト)
  });
}

```

- ベースとなるロジックができたところで動くかを確認するために`ngrok http 3000`を実行。以下のコードの`40a35b6c.ngrok.io`に相当する箇所をコピーする。

```:ターミナル
# 以下を実行したら、なんか出てくるはず
$ ngrok http 3000

    ngrokby@inconshreveable(Ctrl+C to quit)

    Session Status                online
    Session Expires               7 hours, 59 minutes
    Version                       2.2.8
    Region                        United States (us)
    Web Interface                 http://127.0.0.1:4040
    Forwarding                    http://40a35b6c.ngrok.io -> localhost:3000
    Forwarding                    https://40a35b6c.ngrok.io -> localhost:3000
                                      ##### 40a35b6c.ngrok.ioに相当する部分をコピー! ######

    Connections                   ttl     opn     rt1     rt5     p50     p90
                                  0       0       0.00    0.00    0.00    0.00
```

- コピーしたらLINE BOTのチャンネル設定ページの「Webhook URL」の箇所にExpressのルーティングで定義したパスを末尾につけて登録する。私の例だと`40a35b6c.ngrok.io/webhook`となる。
- これでLINE BOTと友だちになってメッセージを送ったらオウム返ししてくれるはず!

###### (2-3) Talk APIにリクエストを送って返事をもらえるようにする
- 一つのファイルにすべてを書くとごっちゃになるので、Talk APIにリクエストを送る部分をモジュール化する。
    - `smallTalk.js`ファイルを作成して、そこにTalk API用のロジックを作成し、`server.js`にインポートする。

```JavaScript:smallTalk.js
// Talk API用のロジックが含まれたファイル

// パッケージのインポート
const request = require('request');


// Talk APIを利用するためにPOSTするURLとAPI Key
const smallTalkApiKey     = 'XXXXXXXXXXXXXXXXXXXXXXXXX';
const smallTalkRequestUrl = 'https://api.a3rt.recruit-tech.co.jp/talk/v1/smalltalk';


// 以下のfunctionをモジュール化
// 引数はユーザーからのテキスト
module.exports = function(userText){

  // Talk APIが受け付けるリクエスト情報
  const smallTalkRequestOption = {
    url: smallTalkRequestUrl,
    form: {
      apikey: smallTalkApiKey,
      query: userText
    }
  };

  // Promiseを返す
  return new Promise((resolve, reject) => {
    // POST リクエストを送信する
    request.post(smallTalkRequestOption, function(err, res, body){
      if(err){
        reject(err);
      }

      // Talk APIから受け取ったstring型のレスポンスをJavaScript Objectにする
      const bodyObj = JSON.parse(body);

      // .results[0].replyはTalk APIからの返信
      if(bodyObj.results[0].reply){
        resolve(bodyObj.results[0].reply);          // 返信がある場合はそれをresolve
      } else {
        reject('Invalid response from smallTalk');  // ない場合はreject
      }
    });
  });
}
```

- `server.js`でこのモジュールをインポートして、ユーザーのテキストを入力して返事を返すようにする。

```JavaScript:server.js
const express = require('express');
/* (略) */
const smallTalk = require('./smallTalk'); // smallTalk.jsでmodule.exportsしたfunctionをそのままインポート

/* (略) */

// LINE BOTがユーザーから受け取ったイベントからいろいろ処理をするイベントハンドラ
// LINE BOTの返信に関してのロジックは全部ここ
// 中身の大幅変更
function handleEvent(event) {

  // ユーザーがテキストを送ったくれた場合のみ反応、それ以外は無視
  if(event.message.type === 'text'){
    const userText = event.message.text; // ユーザーの送ってくれたテキスト

    smallTalk(userText)
      // smallTalkResはTalk APIからの返信(smallTalk.jsでresolveしたやつ)
      .then((smallTalkRes) => {
        return client.replyMessage(event.replyToken, {
          type: 'text',
          text: smallTalkRes
        });
      })
      // エラーだった場合、「...」って返す
      .catch((err) => {
          return client.replyMessage(event.replyToken, {
            type: 'text',
            text: '...'
          });
      });
  } else {
    return Promise.resolve(null);
  }
}
```

- これで会話が成立するようになった!

###### (2-4) 壊れている様子を文字で演出する
- 表現方法は悩んだが、Talk APIの返信の文字をランダムにこんな「ĀçĊëÛÂĬ¶抵ｼ�」感じの文字(以降バグ文字と呼ぶ)で置換する方法で表現してみることにした。
- また、話せば話すほどどんどん壊れていく様子を表したいので、LINEのユーザーIDをキーとしたBOTの壊れ度(以降バグレベルと呼ぶ)のハッシュを作って管理することにした。
- 話せば必ずバグレベルが上がるのではなく、25%くらいの確率でバグレベルが上がるようにした。
- バグレベルは数値で表現し、0で始まり、9を上限とした。このレベルの数だけの文字をバグ文字と置換することにした(Talk APIの返信はそんなに長くないので9でかなり壊れたように見せれると思った)。
- バグ文字は配列で定義してそれをランダムに一つ選び、Talk APIの返信の中のランダムな文字と置換することにした。

```JavaScript:server.js
/* (略) */


// LINEのユーザーIDをキーとしたBOTのバグレベルのハッシュを管理
var userGlitchLevel = {};

// バグ文字の配列
const glitchCharArray = [ /* バグ文字を配列にいっぱい並べる */ ];


// ユーザーのバグレベルを上げるfunction、引数はLINEユーザーのID
function addGlitchLevel(userId){
  // 25%の確率でバグレベルが上がる
  if (Math.random() <= 0.25){
    if(userGlitchLevel[userId] < 9){ // バグレベルのマックスは9
      userGlitchLevel[userId]++;
    }else{
      userGlitchLevel[userId] = 1;
    }
  }
}


// ユーザーのバグレベルを取得、まだハッシュにいない人は0となる
function getGlitchLevel(userId){
  return userGlitchLevel[userId] || 0;
}


// バグ文字に置換するfunction
function glitchifyText(userId, text){
  let returnTextArray = text.split(''); // ユーザーからのテキストを一文字ずつ分けた配列にする

  // ユーザーのバグレベルの数だけループ
  for(let count = 0; count < getGlitchLevel(userId); count++){
    const glitchChar = glitchCharArray[Math.floor(Math.random() * glitchCharArray.length)]; // ランダムなバグ文字を取得
    returnTextArray[Math.floor(Math.random() * returnTextArray.length)] = glitchChar;       // ランダムな位置の文字をバグ文字に置換
  }

  return returnTextArray.join(''); // 最後に配列をもとに戻す
}


// LINEのテキスト返信オブジェクトのラクラク作成function、引数としてLINEのユーザーIDとTalk APIからの返信
function replyText(userId, text){
  const returnText = glitchifyText(userId, text);
  return {type: 'text', text: returnText};
}

/* (略) */

    // handleEventの中のTalkAPIの返信を返すところのロジック
    smallTalk(userText)
      .then((smallTalkRes) => {
        return client.replyMessage(event.replyToken, replyText(userId, smallTalkResponse)); // replyText functionを使うようにした
      })

/* (略) */

```

- これで話していくと徐々にLINE BOTが「壊れていく」様子が文字でわかるようになった。

###### (2-5) "モニター"機能の追加
- モニター機能とは、通信先のBOTの可愛い姿を御尊顔できる機能である。
- LineのMessaging APIにはFlexと呼ばれる返信するメッセージをかっこよくスタイリングできる物があるので、それでモニターっぽくする。
    - [Using Flex Messages](https://developers.line.biz/en/docs/messaging-api/using-flex-messages/ "Using Flex Messages")
- ユーザーが「モニター」と入力したら表示するようにする。
- ちなみにBOTの画像に選んだのは以下のイラスト。なんか世界観とあっている気がした。**もし迷惑でしたらこのLINE BOTからすぐに取り下げます。**
    - [ニコニコ静画の「星宮あき」さんのイラスト](http://seiga.nicovideo.jp/seiga/im4937714?track=seiga_illust_keyword "ニコニコ静画の「星宮あき」さんのイラスト")

![alt](https://i.postimg.cc/GmvQZv2q/1.jpg)

```JavaScript:server.js
/* (略) */

  // handleEventの中、event.message.type === 'text'のif文の中
  if(userText === 'モニター'){
      return client.replyMessage(event.replyToken, {
        type: 'flex',
        altText: 'モニター',
        contents: {
          type: 'bubble',
          header: {
            type: 'box',
            layout: 'vertical',
            contents: [{
              type: 'text',
              text: 'モニター',
              weight: 'bold',
              align: 'center'
            }]
          },
          hero: {
            type: 'image',
            url: <画像URL httpsじゃないとあかんで>,
            size: 'full',
            aspectRatio: '1:1',
            aspectMode: 'cover'
          },
          footer: {
            type: 'box',
            layout: 'vertical',
            contents: [{
              type: 'text',
              text: 'Source: http://seiga.nicovideo.jp/seiga/im4937714?track=seiga_illust_keyword',
              color: '#aaaaaa',
              size: 'xxs'
            }]
          }
        }
      });
    }

/* (略) */
```

- これで「モニター」と入力すると素晴らしいイラストが表示されるようになった。

###### (2-6) 壊れている様子を"モニター"で演出する
- ユーザーのバグレベルによってイラストにノイズなどを表示する。
- それを動的に行う方法はなかったので、すでに処理を行った画像を用意し、URLの配列を用意。**もし画像の加工が禁止行為でしたらこのLINE BOTからすぐに取り下げます。先にお詫び申し上げます。**
- いつも同じだとつまらないので、それぞれのバグレベルの画像を3つほど用意する。つまり2次元配列となる。
- この2次元配列からバグレベルに応じた画像をランダムに表示する。

```JavaScript:server.js
/* (略) */

// バグレベルに応じた画像を格納した2次元配列
const glitchImageArray = [
  [url1, url2, url3], // バグレベル0の画像
  [url1, url2, url3], // バグレベル1の画像
  [url1, url2, url3], // バグレベル2の画像
 /* (略) */
  [url1, url2, url3], // バグレベル7の画像
  [url1, url2, url3], // バグレベル8の画像
  [url1, url2, url3]  // バグレベル9の画像
];

/* (略) */
```

###### (2-7) 最後のディテールをちょいちょい
- LINE BOTを友人として追加したとき、もしくはブロック解除したときにメッセージを表示するようにする。
- 「モニター」といちいち入力するのめんどいのでMessaging APIのquickReply機能を利用する。
    - quickReply機能は、LINEの下側にちょこっと選択肢が出てきて、選択したときに自動的にメッセージを入力してくれたり、位置情報を教えたり、リンク先に遷移したり、いろいろなことをユーザーに行わせることができる機能。
- とりあえずquickReplyで「モニター」と入力させるボタンをすべてのメッセージに付加する。

```JavaScript:server.js
/* (略) */

// [定義済み]
// LINEのテキスト返信オブジェクトのラクラク作成function、引数としてLINEのユーザーIDとTalk APIからの返信
// 追加機能としてquickReplyを追加
function replyText(userId, text){
  const returnText = glitchifyText(userId, text);
  return {
    type: 'text',
    text: returnText,
    quickReply: { // ここがquickReplyのコード
      items:[
        {
          type: 'action',
          action: {
            type: 'message',
            label: 'モニターを見る',
            text: 'モニター'
          }
        }
      ]
    }
  };
}

/* (略) */

  // HandleEventの中、event.message.type === 'text'のif文の前
  // LINE BOTを友人として追加したとき、もしくはブロック解除したときにメッセージを表示するようにする
  if(event.type === 'follow'){
    return client.replyMessage(event.replyToken, {
      type: 'text',
      text: <友達になってくれてありがとうメッセージ>
    });
  }

/* (略) */
```

- これでちょっとユーザビリティ(?)があがった？

#### 3. now.shでネットにデプロイ
###### (3-1) デプロイ前にすること
- 最近(2018/11/9)now.shのバージョンが上がって、よくQiitaにみるデプロイ方法だとうまくいかないので注意。
- 蒸気を回避するにはnow.jsonファイルを作成し、使用するversionを明記(これ大事)、またデプロイ時にURLが変わらないようにaliasの記載。now.shでのurlの雛形は`<alias>.now.sh`。
- now.shでデプロイしたファイルらは公開されるのでLINE BOTやTalk APIで使用したキーをコードの中に生で記入するのではなく、nowの中に環境変数として登録し、それを参照するようにする。以下のリンク先にやり方が記載してあるので参照。
    - [Now.shを使って15分でLINE Botをデプロイする](https://qiita.com/vimyum/items/2226daa74444575811b5 "Now.shを使って15分でLINE Botをデプロイする")

```json:now.json
{
  "version": 1,
  "alias": <URLがalias.now.shとなります>
}
```

###### (3-2) デプロイ
- これで`now -e 環境変数=@secret code...`コマンドを実行すると環境変数の設定とともにdeployが開始される。
- deployが終わったら`now alias`コマンドを実行すると、aliasのURLがちゃんと設定される。


## 完成したもの
ゲームではないですが、ネタバレっぽくなります?

<details><summary>友人として登録したとき</summary>
![3804.jpg](https://qiita-image-store.s3.amazonaws.com/0/317253/7144d242-9cc3-adf1-f375-8f97d9f4bcaf.jpeg)
</details>

<details><summary>初めてモニターで見るとき</summary>
![3793-min.jpg](https://qiita-image-store.s3.amazonaws.com/0/317253/59a0bf16-4c64-50b1-c770-6a95a64efa01.jpeg)
</details>

<details><summary>初めて会話するとき</summary>
![3794-min.jpg](https://qiita-image-store.s3.amazonaws.com/0/317253/18df2301-d49c-74ea-bb24-9badea8be9d4.jpeg)
</details>

<details><summary>壊れてきたときのモニター</summary>
![3798-min.jpg](https://qiita-image-store.s3.amazonaws.com/0/317253/9440b876-bc1d-d373-b8bf-b95fd8206cf8.jpeg)
</details>

<details><summary>壊れてきたときの会話</summary>
![3800-min.jpg](https://qiita-image-store.s3.amazonaws.com/0/317253/bc0b2eb2-10a7-12f7-3e6c-f2ade3e3156f.jpeg)
</details>

<details><summary>完全に壊れたときのモニター</summary>
![3802-min.jpg](https://qiita-image-store.s3.amazonaws.com/0/317253/fefab06a-4fa4-d46e-b368-bd5f27990d2b.jpeg)
</details>

<details><summary>完全に壊れたときの会話</summary>
![3801-min.jpg](https://qiita-image-store.s3.amazonaws.com/0/317253/70d79071-7ba6-1fb9-3e29-bc0b5a33fdb7.jpeg)
</details>


## お友達に?
- 以下のQRコードでともだちになってください!

![1542888124713.jpg](https://qiita-image-store.s3.amazonaws.com/0/317253/c024e171-34a4-e6ef-5507-048d1c00859a.jpeg)


## github
- ここにgithubのリンクおいておきます。
    - https://github.com/verhichi/glitchBot


## イラストについて
使用したイラストは[ニコニコ静画の「星宮あき」さんのイラスト](http://seiga.nicovideo.jp/seiga/im4937714?track=seiga_illust_keyword "ニコニコ静画の「星宮あき」さんのイラスト")です。

フリーアイコンとしてニコニコ静画に載っていますが、画像を加工したことや利用したことに関して問題があればすぐに取り下げます。