LoginSignup
120

More than 1 year has passed since last update.

疑似彼氏、作りました。

Last updated at Posted at 2021-10-19

彼氏を作ろう

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

どんな彼氏?

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

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

環境

Visual Studio Code 1.61.0
Node.js 7.24.0
ngrok

彼氏、できました

実際に作って動かしてみたものがこちら。

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

ソースコード

'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

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

天気について、天気予報APIを使い、「天気は?」と聞いたら今日の東京の天気を教えてくれるように設定。
天気予報の結果に「雨」が入っていた場合、「傘を持って行った方がいいかも」と言ってくれる。
「雨」が入っていなかった場合は、天気予報のみ教えてくれる。

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

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

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

部分一致(固定返信)

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

image_6487327 (3).JPG

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

部分一致(ランダム返信)

//  部分一致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

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

完全一致(固定返信)

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

image_6487327 (2).JPG

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

完全一致(ランダム返信)

// 完全一致したらランダムに返信
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

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

それ以外の場合

// どの条件にも引っかかってない(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

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

まとめ

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

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
120