🤖

timesを全部共有してくれるdiscordボットを更新した

2023/12/02に公開

~この記事はかろ噴水・ゆるゆる Advent Calendar 2023 に参加しています~

https://adventar.org/calendars/9062

はじめに

こんにちは。calloc134 です。

以前、このような記事を執筆しました。

https://zenn.dev/calloc134/articles/c6aeb3d7fec22c

今回は、これを改善したボットを作成したお話をしたいと思います。

前回のボットの問題点

このときに作成したボットなのですが、discord.js を利用しない discord interaction 形式であるため、いくつかの問題がありました。

具体的には、

  • 投稿を指示しても数秒後に投稿される
  • メッセージの時系列がおかしくなる
  • メッセージが投稿されない

などの問題が発生していました。
要するに、動作が不安定であるということです。

そこで、discord.js を利用したコードでボットを作り直すことを決意しました。

要件定義

今回は、この欠点を克服可能な要件を定義しました。

  • discord.js を利用する
    • そのためには常時起動のサービスを選択する必要がある
  • データベースを利用する
    • そのためにはデータベースの組み込み可能なサービスを選択する必要がある

サービス選択

以前利用していた cyclic.sh というサービスではサーバレスでオンデマンドな動作を行うため、常時起動の求められる discord.js を利用することができません。

また、データベースの組み込みができるサービスで、かつ無料のサービスを利用する必要があるため、選択肢は更に限られます。

しかし、そのような厳しい要件をクリアする、Zeabur というサービスを見つけました。

https://zeabur.com/

このサービスは Heroku のようにコードをデプロイできるサービスであり、また常時起動が可能であるため、discord.js を利用することができます。

以下の画面のように、プロジェクトを作成してその内部にサービスを作成することができます。

デプロイできるサービスに、Github からのデプロイと、Prebuiltと呼ばれるものが存在します。

このPrebuiltというものはあらかじめ構築済みのプリセットのようなものですが、その中には MySQL や MongoDB などのデータベース、さらにはあらゆる oss が用意されています。

個人的にはこのシステムがすごく好きです。

その他、環境変数のバインドや、ビルドコマンドとスタートコマンドの指定、ドメインの設定なども可能です。

Zeabur ですが、今回のようなサービスを利用するには、有料プランに加入する必要があります。

(一応、無料プランでも 7 日間は自動で運用でき、7 日を過ぎても手動でログインして延長することができます。)

しかし、Zeabur のドキュメントの日本語翻訳のコントリビュートをいくつか行ったことで、運営の方から無料プランを提供していただきました。

https://github.com/zeabur/zeabur/pull/268

この場を借りて、感謝を申し上げます。

これからもどんどんドキュメントコントリビュートがんばるぞ~

バックエンドの設計

今回は Typescriptdiscord.js を利用し、スラッシュコマンドの実装は fastifydiscord-interactions を利用しています。

また、データベースは PostgreSQL を利用しました。
データベースの ORM として Drizzle ORM を利用しています。自分はいつも Prisma を利用しているのですが、たまには別の選択肢を試してみたくなり、これを利用しました。

更に、データベースに毎回アクセスするのは非効率なため、キャッシュとして node-cache-manager を利用しました。
これによって、データベースへのアクセスを減らすことができます。
node-cacheのようなライブラリの利用も検討しましたが、node-cache-managerの方が機能が豊富であり、かつ esm に対応していたため、こちらを利用しました。
node-cache-managerは、ストアエンジンを redis に切り替えるだけで、メモリキャッシュから redis キャッシュに容易に切り替え可能な点も評価しました。

https://github.com/node-cache-manager/node-cache-manager

ビルドには SWC を利用しました。また、今回はビルド後の環境で Native ESM での実行をする試みを行いました。そのときに拡張子問題に遭遇したので、その解決策としてts-add-js-extensionというユーティリティを利用しました。

https://github.com/GervinFung/ts-add-js-extension

実装

このように実装しました。

https://github.com/calloc134/times_shower_v02

データベーススキーマは以下のようになります。

データベーススキーマ
const channel_list = pgTable("channel_list", {
  ulid: text("ulid").primaryKey().notNull(), // ULID
  channel_id: text("channel_id").notNull(), // チャンネルID
  type: integer("type").notNull(), // チャンネルのタイプ
  // 0: 送信元チャンネル
  // 1: 転送先チャンネル
});

それほど複雑ではないです。

今回、データベースの情報とキャッシュはクロージャに保持させるようにしました。

クロージャに保持させるようにしたコード
const dbClosure = async (db_url: string) => {
  // ポスグレに接続してクライアントを作成
  const db = drizzle(postgres(db_url));

  // キャッシュの設定
  const memory_cache = await caching("memory", {
    max: 100,
    ttl: 60 * 1000,
  });

(...)
  return {
    getSourceChannelList,
    getTargetChannelList,
    addSourceChannelList,
    addTargetChannelList,
    removeSourceChannelList,
    removeTargetChannelList,
  };
};

チャンネル一覧を読み取るとき、キャッシュが存在する場合はキャッシュを返し、存在しない場合はデータベースから読み取ります。

ここで、送信元チャンネルにおいては要素の存在を判定するため、Set としてキャッシュを保持します。
これは、Array.includesよりもSet.hasの方が高速であるという情報に基づいています。
https://qiita.com/kei-nakoshi/items/7d02eae7a0609faab85e

Set.hasについては該当ユーザが任意のチャンネルに投稿するたびに呼び出されるため、高速化が求められます。

送信元チャンネルのデータを返却する関数
// 送信元チャンネルのデータを返却する関数
const getSourceChannelList = async () => {
  // もしキャッシュにデータがあればそれを返却する
  const cache = (await memory_cache.get("source_channel_list")) as
    | Set<string>
    | undefined;
  if (cache) {
    console.debug("cache hit", cache);
    return cache;
  }

  // データベースから送信元チャンネルのデータを取得する
  const channelList = await db
    .select({
      channel_id: channel_list.channel_id,
    })
    .from(channel_list)
    .where(
      eq(channel_list.type, 0) // 送信元チャンネル
    );

  // 結果を求める
  const result = new Set(channelList.map((channel) => channel.channel_id));

  // キャッシュにデータを追加する
  await memory_cache.set("source_channel_list", result);

  console.debug("cache miss", result);

  // 結果を返却する
  return result;
};

また、転送先チャンネルにおいては、要素の存在を判定する必要がないため、Array としてキャッシュを保持します。

転送先チャンネルのデータを返却する関数
// 転送先チャンネルのデータを返却する関数
const getTargetChannelList = async () => {
  // もしキャッシュにデータがあればそれを返却する
  const cache = (await memory_cache.get("target_channel_list")) as
    | Array<string>
    | undefined;
  if (cache) {
    console.debug("cache hit", cache);
    return cache;
  }

  // データベースから転送先チャンネルのデータを取得する
  const channelList = await db
    .select({
      channel_id: channel_list.channel_id,
    })
    .from(channel_list)
    .where(
      eq(channel_list.type, 1) // 転送先チャンネル
    );

  // 結果を求める
  const result = channelList.map((channel) => channel.channel_id);

  // キャッシュにデータを追加する
  await memory_cache.set("target_channel_list", result);

  console.debug("cache miss", result);

  // 結果を返却する
  return result;
};

データが更新された時は、キャッシュを削除します。

送信元チャンネルのデータを追加する関数(例)
// 送信元チャンネルのデータを追加する関数
const addSourceChannelList = async (channel_id: string) => {
  // データベースに送信元チャンネルのデータを追加する
  await db.insert(channel_list).values({
    ulid: ulid(),
    channel_id: channel_id, // チャンネルID
    type: 0, // 送信元チャンネル
  });

  // キャッシュを削除する
  await memory_cache.del("source_channel_list");
};

discord.js とは別に、fastify と discord-interactions を利用してスラッシュコマンドを実装しています。

動作例

今回のボットには、送信元チャンネルと転送先チャンネルの二種類があります。

送信元チャンネル

このチャンネルに投稿されたメッセージを、転送先チャンネルに転送します。

転送先チャンネル

転送されたメッセージを、このチャンネルに投稿します。

チャンネル ID の登録

/add_source_channel_idコマンドと/add_target_channel_idコマンドを利用して、チャンネル ID を登録します。

チャンネル ID の確認

/show_source_channel_idコマンドと/show_target_channel_idコマンドを利用して、チャンネル ID を確認します。

チャンネルの削除

/remove_source_channel_idコマンドと/remove_target_channel_idコマンドを利用して、チャンネル ID を削除します。

投稿の作成

先ほど指定した送信元チャンネルに投稿を行います。

すると、転送先チャンネルに投稿が行われます。

大成功!

おわりに

一日溶かしましたが、なんとか完成させることができました。嬉しい!

Zeabur の運営には本当に感謝です!

運営の方が「日本の皆さんにも是非使ってほしい」とおっしゃっていたので、是非使ってみてください!

おまけ

以下は Zeabur の招待コードです。

ここから登録するとかろっくの助けになります、なにとぞ~~~

https://zeabur.com/?referralCode=calloc134

宣伝

この記事はかろ噴水・ゆるゆる Advent Calendar 2023 に参加しています!

https://adventar.org/calendars/9062

かろ噴水とは、自分が運営しているコミュニティです。

招待制ですが、誰でも参加できるようになっているので、興味があったら twitter や discord の@calloc134までご連絡ください!

今回の times ボットもここで稼働しています。

https://twitter.com/calloc134/status/1710308066556604566

GitHubで編集を提案

Discussion