疑似彼氏、作りました。

# 彼氏を作ろう
何かと疲れている現代人。私もまたそのひとりである。
ちょっとしたときめきと癒しが欲しい。仕事帰りに誰かに「今日も一日頑張ったね」と優しく言われたい。私のどうでもいい話に「うんうん」と相槌を打ってくれるだけでいい。
そうだ、**LINE Botで彼氏を作ろう。**

# どんな彼氏？
**優しい言葉をかけてくれる**のは大前提で、**今日の天気**も教えてほしい。（よく傘を忘れてびしょ濡れになるので）
ついでに天気によって会話パターンも変化するとよい。
ということで、やりたいことは以下の2点。

* 天気予報（天気によって返事のパターンあり）
* ある程度キャッチボールのある会話

# 環境
Visual Studio Code 1.61.0
Node.js 7.24.0
ngrok

# 彼氏、できました
実際に作って動かしてみたものがこちら。

https://twitter.com/nnmshita/status/1450278666454241285

紹介します。彼氏の**「タケシ(仮)」**（LINE Bot名）です。
「タケシはきっとラーメン好き」という無駄な設定の元、LINEアイコンをラーメンにした。
（画像は私が週3で通っていた店の明太子油そば。激旨）
私にめちゃめちゃ優しい、彼氏ができてしまった。たまに（通信の関係で）返事がこなくて、既読スルーされるのもご愛敬。

## ソースコード

```javascript
'use strict';

// ########################################
//               初期設定など
// ########################################

// パッケージを使用します
const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');

// ローカル（自分のPC）でサーバーを公開するときのポート番号です
const PORT = process.env.PORT || 3000;

// Messaging APIで利用するクレデンシャル（秘匿情報）です。
const config = {
    channelSecret: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx',  //作成したBOTのチャンネルシークレット
    channelAccessToken: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' //作成したBOTのチャンネルアクセストークン
};


// ########## ▼▼▼ サンプル関数 ▼▼▼ ##########
const sampleFunction = async (event) => {
    const userText = event.message.text;
    let replyText = '';

    if (userText ===  '天気は？') {
        // 「リプライ」を使って先に返事しておきます
        await client.replyMessage(event.replyToken, {
            type: 'text',
            text: 'ちょっと待ってね'
        });
        
        let pushText = '';
        try {
            // axiosで天気予報APIを叩きます（少し時間がかかる・ブロッキングする）
            const res = await axios.get('https://weather.tsukumijima.net/api/forecast/city/130010');
            // 本日の天気を取得
            const today_weather = res.data.forecasts[0].telop;
            // 返信用のテキストを作成
            pushText = `今日の東京の天気は${today_weather}だよ`;
                // 予報に「雨」が含まれている場合は、「傘を持って行った方がいいよ」と言ってくれる
                if (pushText.indexOf(`雨`) > -1) {
                pushText = `今日の東京の天気は${today_weather}だよ。傘を持って行った方がいいかも`;
                }


        } catch (error) {
            pushText = 'ちょっと分からないや。ごめんね';
            // APIからエラーが返ってきたらターミナルに表示する
            console.error(error);
        }

            // 「プッシュ」で後からユーザーに通知します
            return client.pushMessage(event.source.userId, {
                type: 'text',
                text: pushText,
            });

    }

    // 部分一致1（「ねえ」という文字が1ヶ所でも含まれていたら反応）
    if (userText.indexOf('ねえ') > -1) {
        replyText = 'どうしたの？';
    }

    // 部分一致2（「曜」という単語が1ヶ所でも含まれていたら反応）
    if (userText.indexOf('曜') > -1) {
        replyText = '今週もあとちょっとだね！週末は何して過ごそうか？';
    }

    //  部分一致3（「れた」という単語が1ヶ所でも含まれていたら反応）※疲れた、つかれたの表記のぶれ用
    if (userText.indexOf('れた') > -1) {
        // 返事
        const attractions = ['お疲れさま', '今日も頑張ったね', 'ごはん食べに行く？', '美味しいものでも食べに行こう', 'お仕事してえらい！'];
        // 返事の種類数
        const att_count = attractions.length;
        // 乱数（0以上1未満の小数）* 種類数 をして小数以下切り捨て
        const index = Math.floor(Math.random() * att_count);
        // インデックスを指定して特定の返事を示す文字列を取り出す
        replyText = attractions[index] ;
    }

    //  部分一致4（「おはよ(う)」という単語が1ヶ所でも含まれていたら反応）
    if (userText.indexOf('おはよ') > -1) {
        // 返事
        const attractions = ['おはよう', 'おはよー','おはよ。よく寝れた？',  'おはよ。眠いよー'];
        // 選択の種類数
        const att_count = attractions.length;
        // 乱数（0以上1未満の小数）* 種類数 をして小数以下切り捨て
        const index = Math.floor(Math.random() * att_count);
        // インデックスを指定して特定の返事を示す文字列を取り出す
        replyText = attractions[index] ;
    }

    // 部分一致5（「好き！」という単語が1ヶ所でも含まれていたら反応）
    if (userText.indexOf('好き！') > -1) {
        // 返事
        const attractions = ['僕も！', '好きー！', '大好きー！', '嬉しいな！', 'やったー！'];
        // 選択の種類数
        const att_count = attractions.length;
        // 乱数（0以上1未満の小数）* 種類数 をして小数以下切り捨て
        const index = Math.floor(Math.random() * att_count);
        // インデックスを指定して特定の返事を示す文字列を取り出す
        replyText = attractions[index] ;
    }

    // 完全一致したらランダムに返信
    if (userText === '私のこと好き？') {
        // 返事
        const attractions = ['好きだよ！', '大好き！！', 'きみは？', '恥ずかしいな・・・'];
        // 返事の種類数
        const att_count = attractions.length;
        // 乱数（0以上1未満の小数）* 種類数 をして小数以下切り捨て
        const index = Math.floor(Math.random() * att_count);
        // インデックスを指定して特定の返事を示す文字列を取り出す
        replyText = attractions[index] ;
    }

    // 完全一致したら指定の文言を返信
    if (userText === '無理') {
        // 返事        
        replyText = 'イケるっしょ！';
    }


    // この時点でどの条件にも引っかかってない（replyTextが空文字列のまま）なら相槌をうっておく
    if (replyText === '') {
        const attractions = ['うんうん、そうだよね', 'そういうこともあるよね', 'そうなんだ', 'うん', '頑張りすぎないようにね','うんうん'];
        // 返事の種類数
        const att_count = attractions.length;
        // 乱数（0以上1未満の小数）* 種類数 をして小数以下切り捨て
        const index = Math.floor(Math.random() * att_count);
        // インデックスを指定して特定の返事を示す文字列を取り出す
        replyText = attractions[index] ;
    }
    
    return client.replyMessage(event.replyToken, {
        type: 'text',
        text: replyText
    });
};

// ########## ▲▲▲ サンプル関数 ▲▲▲ ##########



// ########################################
//  LINEサーバーからのWebhookデータを処理する部分
// ########################################

// LINE SDKを初期化します
const client = new line.Client(config);

// LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます
async function handleEvent(event) {
    // 受信したWebhookが「テキストメッセージ以外」であればnullを返すことで無視します
    if (event.type !== 'message' || event.message.type !== 'text') {
        return Promise.resolve(null);
    }
    // サンプル関数を実行します
    return sampleFunction(event);
}


// ########################################
//          Expressによるサーバー部分
// ########################################

// expressを初期化します
const app = express();

// HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします
app.post('/webhook', line.middleware(config), (req, res) => {
  
  // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行
  if (req.body.events.length === 0) {
    res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します（なくてもよい）
    console.log('検証イベントを受信しました！'); // ターミナルに表示します
    return; // これより下は実行されません
  } else {
    // 通常のメッセージなど … Webhookの中身を確認用にターミナルに表示します
    console.log('受信しました:', req.body.events);
  }

  // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、
  // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します
  Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});

// 最初に決めたポート番号でサーバーをPC内だけに公開します
// （環境によってはローカルネットワーク内にも公開されます）
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);

```
全体のコードは上記。
詳細については、下記に記載。

### 天気予報API

```Javascript
if (userText ===  '天気は？') {
    // 「リプライ」を使って先に返事しておきます
    await client.replyMessage(event.replyToken, {
         type: 'text',
         text: 'ちょっと待ってね'
    });
        
    let pushText = '';
    try {
        // axiosで天気予報APIを叩きます（少し時間がかかる・ブロッキングする）
        const res = await axios.get('https://weather.tsukumijima.net/api/forecast/city/130010');
        // 本日の天気を取得
        const today_weather = res.data.forecasts[0].telop;
        // 返信用のテキストを作成
        pushText = `今日の東京の天気は${today_weather}だよ`;
            // 予報に「雨」が含まれている場合は、「傘を持って行った方がいいよ」と言ってくれる
            if (pushText.indexOf(`雨`) > -1) {
            pushText = `今日の東京の天気は${today_weather}だよ。傘を持って行った方がいいかも`;
            }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122226/564d6a90-4e2e-dde9-9edc-318dd9974e4a.png)

天気について、[天気予報API](https://weather.tsukumijima.net/)を使い、「天気は？」と聞いたら今日の東京の天気を教えてくれるように設定。
**天気予報の結果に「雨」が入っていた場合、「傘を持って行った方がいいかも」と言ってくれる。**
「雨」が入っていなかった場合は、天気予報のみ教えてくれる。

天気予報の地域の指定は、天気予報APIの[全国の地点定義表](https://weather.tsukumijima.net/primary_area.xml)でコードを確認し、`https://weather.tsukumijima.net/api/forecast/city/`の後ろに記載する。（東京は130010）

```Javascript
const res = await axios.get('https://weather.tsukumijima.net/api/forecast/city/130010');
```

天気によって返信パターンを変える仕様について、上手く条件が指定できずに苦労した。
とりあえず表示できるようにしたが、もう少しよい方法があるような気がするので、今後時間をかけて追究していきたい。


### 部分一致（固定返信）

```Javascript
// 部分一致2（「曜」という単語が1ヶ所でも含まれていたら返信）
if (userText.indexOf('曜') > -1) {
    replyText = '今週もあとちょっとだね！週末は何して過ごそうか？';
}
```

![image_6487327 (3).JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122226/cfd455ed-5e48-e5fb-5aef-7bab30a5d3bf.jpeg)

曜日に関して話しかけたら励ましてもらいたいな、と思い設定。
どの曜日を入れても反応してもらえるように、こちらからの問いかけに「曜」の文字が入っていたら固定の返信をしてくれるようにした。
日曜でも月曜でも「今週もあとちょっとだね！」と返してくれる。タケシ、可愛いな。

### 部分一致（ランダム返信）

```Javascript
//  部分一致3（「れた」という単語が1ヶ所でも含まれていたらランダムに返信）※疲れた、つかれたの表記のぶれ用
if (userText.indexOf('れた') > -1) {
    // 返事
    const attractions = ['お疲れさま', '今日も頑張ったね', 'ごはん食べに行く？', '美味しいものでも食べに行こう', 'お仕事してえらい！'];
    // 返事の種類数
    const att_count = attractions.length;
    // 乱数（0以上1未満の小数）* 種類数 をして小数以下切り捨て
    const index = Math.floor(Math.random() * att_count);
    // インデックスを指定して特定の返事を示す文字列を取り出す
    replyText = attractions[index] ;
}
```

![image_6487327.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122226/e507ce0f-b32f-5717-66fe-b325936f1674.jpeg)

「疲れた」「つかれた」の漢字とひらがな両方に対応するため、「れた」の言葉に反応するように設定。
複数の返信パターンをランダムで返すようにしている。
ここでは返信パターンは5個で設定しているけれど、5個以下でも可。5個以上も設定できるので、必要に応じてパターンを追加する。


### 完全一致（固定返信）

```Javascript
// 完全一致したら固定の返信
if (userText === '無理') {
   // 返事        
   replyText = 'イケるっしょ！';
}
```

![image_6487327 (2).JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122226/3f10df2d-6ee1-1783-4fdd-e5b87a83b2dd.jpeg)

「無理」と弱音を吐いたら、「イケるっしょ！」と元気に励ましてくれる。
タケシ、いい子だね・・・


### 完全一致（ランダム返信）

```Javascript
// 完全一致したらランダムに返信
if (userText === '私のこと好き？') {
    // 返事
    const attractions = ['好きだよ！', '大好き！！', 'きみは？', '恥ずかしいな・・・'];
    // 返事の種類数
    const att_count = attractions.length;
    // 乱数（0以上1未満の小数）* 種類数 をして小数以下切り捨て
    const index = Math.floor(Math.random() * att_count);
    // インデックスを指定して特定の返事を示す文字列を取り出す
    replyText = attractions[index] ;
}
```

![image_6487327 (1).JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122226/46cb808f-fcba-0841-6fe3-6f37935e6238.jpeg)

「私のこと好き？」と聞くと、ランダムで返信してくれる。
そろそろ狂気を感じてきましたか？

### それ以外の場合

```JavaScript
// どの条件にも引っかかってない（replyTextが空文字列のまま）なら相槌をうつ
if (replyText === '') {
    const attractions = ['うんうん、そうだよね', 'そういうこともあるよね', 'そうなんだ', 'うん', '頑張りすぎないようにね'];
    // 返事の種類数
    const att_count = attractions.length;
    // 乱数（0以上1未満の小数）* 種類数 をして小数以下切り捨て
    const index = Math.floor(Math.random() * att_count);
    // インデックスを指定して特定の返事を示す文字列を取り出す
    replyText = attractions[index] ;
}
```

![image_6487327 (4).JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122226/564b73ac-9960-7642-81a0-e9e82a509b36.jpeg)

設定した言葉以外の問いかけをした場合、ランダムで相槌を打つように設定。

# まとめ
夜中に思いつき、深夜テンションのまま勢いで作成。
シンプルな仕組みなので、一致の条件や選択肢の種類にバリエーションを持たせて楽しく作成できた。
時間の都合で簡単なものになってしまったけれど、[ホットペッパーAPI](https://webservice.recruit.co.jp/doc/hotpepper/reference.html)というのも見つけたので、今後はタケシ（LINE Bot）に実装して、お店検索もできる彼氏にしたい。
彼氏改造計画の夢は広がる。


# 参考

* [1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest]
(https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)
* [【JavaScript】～を含むかのチェック](https://qiita.com/kazu56/items/557740f398e82fc881df)