👾

なろう小説「Re:ゼロから始める異世界生活」の最新話更新をお知らせするLINE Botを作ったYO

2021/07/31に公開

はじめに

初投稿失礼いたします (っ_ _)っ

皆さん、"リゼロ"ってご存知でしょうか。
この作品、アニメはもちろん、原作小説の方もかなり面白いんですよね。
2021 年 7 月現在でも更新は続いていて、僕もかなり楽しませてもらっています。

ただ、作者の 長月達平先生 はかなり多忙な方らしく、不定期で更新されているという現状です。
(最近は「Vivy」関係でばたばたしているのかなぁと思ったり)

というわけで、最新話が投稿されたときに「俺がいち早く読んでやるぜ」と思い、今回の Bot を作ることにしました。

原作小説は こちら から読むことができます。

こちらは先日のつぶやき。


今回作った LINE Bot は、とりあえず Cloud Functions と Firestore を使ってみたかったということもあり、あまりデータベースを有効活用できていません。
似たようなものを作るのであれば、データは CSV ファイル等管理した方がいいかもです。

Github にコードをあげているので、興味のある方は覗いてみてください。

使用技術

  • Node.js
  • Cloud Functions
  • Firestore Database
  • LINE Message API

Bot の 構成

ここでは、最初に Bot 全体の構成を確認した後、データベースの構成を見ていきます。

全体的な構成

今回作成した Bot は、5 分おきに以下の処理を繰り返すことで動いています。

  1. 小説家になろう の API を用いてリゼロの最新情報を取得する
  2. Firestore に保存しているリゼロの最終投稿日と総話数を取得する
  3. 上記を比較し、最終投稿日と総話数が更新されていたら LINE で通知を放つ
  4. LINE の通知とともに Firestore の情報を更新する


図で表すとこんな感じです。

前述したように、この構成では保存するデータ量が増加することは無いため、わざわざ Firestore に情報を格納する必要はありません。

適当に CSV ファイルにでも書き込んでおけば OK です。

Firestore の構成

Firestore の構成は次のようになっています(test は無視してください)。

rezero > info の中に必要な情報を格納しており、各プロパティは以下に示す情報と対応しています。

  • lastPosted: 最終投稿日
  • latestStory: 投稿されている総話数
  • title: 小説のタイトル


こちらの構成ですが、リゼロ以外の作品の更新も通知してほしいならば、
<追加するコレクション名> > info として上記の 3 つのプロパティを用意することで、簡単に追加することができます。

実装コード

今回作成した Bot は、以下に示す 6 つのモジュールで構成されています。

  • index.ts
  • checkUpdate.ts
  • fetchNaroInfo.ts
  • readDB.ts
  • sendBroadCastMessage.ts
  • updateDB.ts


それでは Bot のワークフローに沿って、各モジュールの紹介をしていきます。

基本的に関数のみを掲載しているため、全てのコードを見たい方は Github から確認お願いします。

index.ts

Cloud Functions では 5 分おきにindex.ts内にあるscheduledFunction関数を実行するように設定しています。

index.ts
exports.scheduledFunction = functions
  .region("asia-east2")
  .pubsub.schedule("every 5 minutes")
  .onRun(async (_) => {
    const collections = [{ name: "rezero", ncode: "N2267BE" }];
    for (const collection of collections) {
      await checkUpdate(collection);
    }
    return null;
  });

関数内のconst collections = [{ name: "rezero", ncode: "N2267BE" }];は、name プロパティと ncode プロパティを持つオブジェクトから成るリストとなっており、各プロパティは以下の情報と対応しています。

  • name: Firestore のコレクション名
  • ncode: なろう小説の各作品が持つユニークな ID

このリスト内にあるオブジェクトを、checkUpdate 関数に渡していきます。

余談ですが、こちらに別作品のオブジェクトを追加することで、機能を拡張することができます。

checkUpdate.ts

ここでは、「情報が更新されているのかを確認し、更新があれば LINE にて通知を放ち、データベースも更新する」という処理を行います。

checkUpdate.ts
export const checkUpdate = async (
  collection: CollectionType
): Promise<void> => {
  const dbData = await readDB(collection.name);
  const apiData = await fetchNaroInfo(collection.ncode);

  if (dbData === undefined || apiData === undefined) {
    await updateDB(collection.name, apiData);
    return;
  }

  const dbTS = Date.parse(dbData.lastPosted);
  const apiTS = Date.parse(apiData.lastPosted);
  if (dbTS < apiTS && dbData.latestStory < apiData.latestStory) {
    sendBroadCastMessage(collection.ncode, apiData);
    updateDB(collection.name, apiData);
  }
};

まずは、引数に渡されたオブジェクトをもとに、readDB関数とfetchNaroInfo関数を実行し、データベース内の情報と作品の最新情報の2つを取得します。

この段階でデータベースに初期値が入っていない場合は、updateDB関数を用いて最新情報を追加しておきます。

関数内の最後の 6 行で、最終投稿日と総話数が更新されているかを判定し、更新があればsendBroadCastMessage関数とupdateDB関数を実行します。

ここまでで全体的な流れはおしまいです。
ここからは、上で使用している関数を流しで紹介していきます。

readDB.ts

Firestore 内にあるデータを取得する関数です。

readDB.ts
export const readDB = async (
  colName: string
): Promise<FirebaseFirestore.DocumentData | undefined> => {
  const docName = "info";
  const ref = db.collection(colName).doc(docName);
  const doc = await ref.get();
  return doc.data();
};

fetchNaroInfo.ts

小説家になろうの APIを用いて、指定した作品の情報を取得する関数です。

fetchNaroInfo.ts
export const fetchNaroInfo = async (ncode: string): Promise<DataType> => {
  const url = "https://api.syosetu.com/novelapi/api/";
  const params = {
    out: "json",
    ncode: ncode,
  };
  const data = {} as DataType;
  await axios
    .get(url, { params })
    .then((res) => {
      const info = res.data[1];
      data.title = info.title;
      data.lastPosted = info.general_lastup;
      data.latestStory = info.general_all_no;
    })
    .catch((err) => {
      console.error(err);
    });
  return data;
};

API から取得したデータは、それぞれ以下の情報と対応します。

  • title: Firestore の title プロパティ
  • general_lastup: Firestore の lastPosted プロパティ
  • general_all_no: Firestore の latestStory プロパティ

sendBroadCastMessage.ts

LINE アカウントにて、友だち全員に通知を放つ関数です。

sendBroadCastMessage.ts
export const sendBroadCastMessage = async (
  ncode: string,
  apiData: DataType
): Promise<void> => {
  const naro_url = "https://ncode.syosetu.com";
  await client.broadcast({
    type: "text",
    text: `"${apiData.title}" の最新話が投稿されました!\n\n${naro_url}/${ncode}/${apiData.latestStory}`,
  });
};

broadcast関数は LINE SDK が用意している関数で、これを用いることで、友だち追加しているユーザー全員に通知を放つことができます。

また、なろう小説の URL はhttps://ncode.syosetu.com/[ncode]/[最新話数]のように構成されており、それぞれ埋めることで最新話の URL を作成しています。

updateDB.ts

Firestore 内の情報を更新する関数です。

updateDB.ts
export const updateDB = async (
  colName: string,
  data: DataType
): Promise<void> => {
  const docName = "info";
  const ref = db.collection(colName).doc(docName);
  await ref.set({
    title: data.title,
    lastPosted: data.lastPosted,
    latestStory: data.latestStory,
  });
};

おわりに

Bot の構成は割とシンプルだったのですが、初めて Cloud Functions を使用したため、案外手こずったというのが正直な感想です。

特に最初にデプロイしたときなんかは、エラーが発生してもその詳細が見れないという症状にハマり、結構時間を持っていかれてしまいました…

この一件は、functions/package.json に LINE SDK のモジュールを記載していなかったことが原因だったのですが、関数を1つ1つコメントアウトしながらやっとの思いで見つけることができたので苦労しましたね。

ただ、Bot を作った 2 日間で、Cloud Functions の使い方と Firestore の使い方の両方に触れることができたので満足しております!


それでは、これからもリゼロの通知を待ちながら"強く生きていこう"と思います。

最後まで読んでいただきありがとうございました!!

参考にした記事

Discussion