超自分好みのなろう小説を検索してくれるLINE Botを作ってみた

<img width="" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2775528/8f361968-6644-00d1-d051-63285e56fac7.png">

## 好きななろう小説はありますか？
筆者は[小説家になろう](https://syosetu.com/)が好きで日々新たな作品との出会いを求めています。
検索機能も存在しておりよく使うのですが、非常に作品数が多く検索条件も幅広いためタイトルで惹きつけられるものの、読んでみたら自分の好きな感じではない、ということも多くありました。

そんな中、なろう小説のAPIが存在することを発見しました。
スマホで小説を読むことも多かった私としてはLINEと連携させることで超自分好みの条件で小説を検索・レコメンドするLINE Botができるのではと思い製作を開始しました。

## 自分好みのなろう作品検索Botが誕生しました。
キーワードを入力するとランダムで2件の作品が表示されます。
(一回の検索上限が20件だったため3件にすると被りが出やすく面白くありませんでした。)
自分が好きなキーワードはリッチメニュー化して入力の手間を省きました。
自由なキーワードでも検索が可能です。
<img width="300" alt="ezgif.com-gif-maker.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2775528/17a5267b-f445-c65a-8206-c8678532a793.gif">

**友達用のQRコードはこちらです！**
<img width="200" alt="QR.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2775528/8605ae82-db3d-5b9f-1f87-3f73756c9ee8.png">

:::note
まだデプロイ作業が完了しておりませんが、もしよければ友達になってもらえると嬉しいです。
デプロイが完了しましたらまた追記の上、ご連絡いたします。
:::


## 環境とソースコード
### 環境
以下の環境で製作を行っています。
- [Visual Studio Code 1.71.2](https://azure.microsoft.com/ja-jp/products/visual-studio-code/)
- [node.js v18.7.0](https://nodejs.org/ja/)
- [LINE Developers](https://developers.line.biz/en/)
- [なろう小説API](https://dev.syosetu.com/man/api/)

### ソースコード
<details><summary>長いので格納しています。　<font color="Red">**(クリックで表示)**</font></summary>

```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: '*********************',
    channelAccessToken: '*******************'
};

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

const getNovelName = async (userId, tag) => {
    let result1 = '';
    let result2 = '';
    for (let i = 0; i < 2; i++) {
        let num = Math.floor(Math.random() * (20 - 1) + 1);
        try {
            // axiosでなろうAPIを叩きます
            const res = await axios.get('https://api.syosetu.com/novelapi/api/?time=30-&word=' + encodeURI(tag) + '&out=json&type=re&stop=1&order=dailypoint');
            const item = res.data;

            result1 = item[num].title
            result2 = item[num].ncode
            // ターミナルにも検索結果を出しておきます
            console.log(item[num].title);
            console.log(item[num].ncode);
            console.log(num);
            // 正常に取得できればここで終了

            // リプライではなく「プッシュ」を送ります
            // Botからユーザーへ一方的に通知を送ることができる機能です
            await client.pushMessage(userId, {
                type: 'text',
                text: `${result1} \n ${'https://ncode.syosetu.com/' + result2 + '/'}`
            });

        } catch (error) {
            console.log(error);
        }
    }
}

const sampleFunction = async (event) => {
    // ユーザーIDとメッセージ文字列を関数に渡します
    // メッセージ文字列でなろうAPIタグを検索し、結果をなろうAPIから受け取ったらユーザーIDに対して「プッシュ」送信します
    // 検索には少し時間がかかるので、これの結果は後でユーザーに送られます
    getNovelName(event.source.userId, event.message.text);

    // こちらが先に返事を返します
    // ユーザーからのメッセージに対する「リプライ」です
    // リプライは、受信したメッセージ1つにつき1回しか使えません
    return client.replyMessage(event.replyToken, {
        type: 'text',
        text: '検索しています……'
    });
};
// ########## ▲▲▲ サンプル関数 ▲▲▲ ##########

// ########################################
//  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サーバーを実行中です…`);

```
</div></details>

## 製作で工夫した部分
**①とにかく自分好みになるようにAPIをつなげる**

```javascript:叩いているAPI(ソースコードより抜粋)
'https://api.syosetu.com/novelapi/api/?time=30-&word=' + encodeURI(tag) + '&out=json&type=re&stop=1&order=dailypoint'
```
なろう小説APIを読んでいると多数の検索条件があることが分かったので
できるだけ自分好みになるように以下のように設定しました。
1. ある程度読み応えがある(読み終わるのに30分以上かかる) : `time=30-`
2. 今も続いているか、もしくは完結している : `type=re`
3. 熱量高めの記事がいい(長期連載停止をしていない) : `stop=1`
4. 抽出される小説が固定化されない : `order=dailypoint` 

**②日本語のワード検索が行えるように注意** 
なろう小説APIを参照すると単語検索ができるようになっています。
日本語でもそのまま叩いてくれると思われましたが、2byte以上でないと
実行がされないため、` + encodeURI(tag) + `を使うことにしました。

**③記事にアクセスしやすくなるようにコードを取得してURL化する**
```javascript
const getNovelName = async (userId, tag) => {
    let result1 = '';
    let result2 = '';
    for (let i = 0; i < 2; i++) {
        let num = Math.floor(Math.random() * (20 - 1) + 1);
        try {
            // axiosでなろうAPIを叩きます
            const res = await axios.get('https://api.syosetu.com/novelapi/api/?time=30-&word=' + encodeURI(tag) + '&out=json&type=re&stop=1&order=dailypoint');
            const item = res.data;

            result1 = item[num].title
            result2 = item[num].ncode
            // ターミナルにも検索結果を出しておきます
            console.log(item[num].title);
            console.log(item[num].ncode);
            console.log(num);
            // 正常に取得できればここで終了

            // リプライではなく「プッシュ」を送ります
            // Botからユーザーへ一方的に通知を送ることができる機能です
            await client.pushMessage(userId, {
                type: 'text',
                text: `${result1} \n ${'https://ncode.syosetu.com/' + result2 + '/'}`
            });

```
なろう小説APIではURLを直接叩いてくることはできないので
Nコーデというなろう小説の作品ごとに割り振られているユニークコードと
前半部分の固定URLをつなげることで、LINE上でURLを表示することを可能にしました。

**④JSONの構造に注意する**
JSON構造を参照すると0の時にはallcountのみしか情報がありませんでした。
なので記事のタイトルや③のNコードを取得する際には1以上の整数にする必要がありました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2775528/b35015da-d2bf-ab34-5758-b7a28e8d2010.png)

## これであなたもなろう好きの一員に！
何度も好きな作品は読み返すタイプなので特定のメッセージを送信した際にはお気に入り作品を表示する機能も実装したいと思いましたが、思うように進まず断念しました。
また今回は20件の中からランダムで2件を抽出していますが、もっと多くの母数から検索ができたほうが新しい出会いがありそうに思いました。
またエラーが起こった際には特に今は通知が飛ばないのでそれもちょっと不親切だなと思っています。
歯がゆい部分もありますが、今後も製作を続けてみようと思います。