PLiCy新作WWA通知機構

## 概要
WWADicscordコミュニティにPLiCyに新着WWAが投稿されると通知するbotを作りました。
今回はこの通知botの動作原理を半分備忘録も兼ねてご紹介したいと思います。

![#wwa-plicy新作通知 _ World Wide Adventure - Discord 2024_11_09 4_14_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179706/92d127ef-524c-12ab-1d6b-67a0901afb9c.png)

この記事は [WWA Advent Calendar 2024](https://adventar.org/calendars/10185) の3日目の記事になります。

https://adventar.org/calendars/10185

## 構成
構成としては実際の動作にはTypeScriptと [node-cron](https://www.npmjs.com/package/node-cron) を使って動作しています。
実行環境はVPSサーバーにNodeコンテナを立て、その上で実行させています。

[discord.js](https://www.npmjs.com/package/discord.js?activeTab=readme) を使う手も考えたのですが、手元のWSL2のDebian環境がやや古くてNode v16までしか入らず、discord.jsの実行にはNode v18以上の実行が必要なことと、Discordの発言を検知するのではなく、一定時間おきに新着WWAを検知して投稿する[ポーリング](https://e-words.jp/w/%E3%83%9D%E3%83%BC%E3%83%AA%E3%83%B3%E3%82%B0.html)する形式で動作するので単純にDiscordのwebhookを叩けばいいかなと思い採用しました。

ちなみに、ちゃんとdiscord.jsを使って動くbot（テムたんbot）も作ったことがあるのですが、残念ながら今は動いてないです（笑）

https://github.com/hirarira/discordTemtan

## Node.jsサイドのロジック
PLiCy管理人のシャイんさんよりPLiCyに投稿された新着WWAを取得するエンドポイントをいただきました。

ただ、肝心のエンドポイントについては頻繁にアクセスされると負荷が大きくなってしまうので表に出さないでほしいとの要望があったので申し訳ないですが非公開とさせてください。

新着WWA取得エンドポイントからは以下のような形で新着WWAデータを20件取得出来ます。

```json
{
    "game": [
        [
            192531,
            "//img.plicy.net/ImageLoad?HashFile=0257aa97627f6afae4708dda47962ec5",
            "アップグレードダンジョン　体験版",
            "",
            "とても広いケーブダンジョン風マップを攻略するＷＷＡです。赤いスライムの見た目のアイテムを使用すると..",
            6,
            "2024年11月06日",
            false,
            "//img.plicy.net/ImageLoad?HashFile=33e7c7b3b79a2c2a2bb3d94ffdd0ddf7",
            240,
            0,
            5,
            "(￣ー￣) 遊びやすさはイマイチですが、中々面白い作品だと思いました （；＿；） 楽しみにしてます （　｜＿｜）（｜＿｜　） "
        ]
    ]
}
```

上から順に「ゲームID」「サムネURL」「タイトル名」「キャッチコピー」「説明文」「不明」「投稿日」…と並んでます、

![FireShot Screen Capture #347 - 'アップグレードダンジョン　体験版を遊ぶ！　フリーゲームのPLiCy [プリシー]' - plicy.net.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179706/afcdfed9-a9b8-c50f-8f75-cb0a794b86e0.png)

PLiCyに投稿されたゲームページの概要を比較すると、作者情報は取れないもののゲームタイトルやキャッチコピーの全文と、コメントの冒頭50文字程度が取れることがわかります。

そのままだと単純な配列で扱いにくいのでこんな感じでObjectに変換し、WWAData型の配列として、取得できたWWAデータ一覧を保管します。

```typescript
const response = await axios.get(url);
const gameListRowData: { game: string[] } = response.data;
return gameListRowData.game.map((data) => {
  return {
    id: Number(data[0]),
    miniImageURL: getPLiCyGameImageURL(data[1]),
    title: data[2],
    catchphrase: data[3],
    text: data[4],
    postDate: data[6],
    imageURL: getPLiCyGameImageURL(data[8]),
    url: `https://plicy.net/GamePlay/${data[0]}`,
  };
});
```

WWADataの形はこんな形で定義してます。
```typescript
interface WWAData {
  id: number,
  miniImageURL: string,
  title: string,
  catchphrase: string,
  text: string,
  postDate: string,
  imageURL: string,
  url: string
}
```

ただこのままだと最新20件全部が投稿対象となってしまうので、既にDiscordに投稿済みかを判断する必要があります。

本来ならDBを立てて管理する必要があるのですが、今回は件数がそこまで大きくないのでサーバ上に取得済みのWWAデータを `postWWAData.json` という名前で保存しています。

ゲーム名で投稿済みかどうかを判断すると同一名称のゲームが複数投稿されたときに投稿済みとして判断されるため、ゲームを一意に識別するためのゲームIDを使って投稿済みかを判断します。

また、初回起動時は投稿済みデータが存在しないため、このままだと初回起動時にdiscordにゲーム通知が20件飛んでしまうため例外処理として、投稿済みデータが1件も無いときにはdiscord投稿を行わないようにしています。

```typescript
// 新規投稿WWAがあればDiscordに投稿
async function checkNewWWAAndPostDiscord() {
    // Discord投稿済みのゲーム一覧
    const postedWWADataList: WWAData[] = await readPostedWWAData();
    // ポスト済みのデータがない場合にはDiscord投稿をしない
    const isPostDiscord = postedWWADataList.length > 0;
    // 最新20件のゲームを取得する
    const gameDataList: WWAData[] = await getPLiCyWWANewGame();
    // 未投稿のゲーム一覧を抽出
    const noPostWWAData = gameDataList.filter((gameData) => {
        // 未投稿ゲームかを判断
        const isMatch = !postedWWADataList.some((pwwa) => {
          return pwwa.id === gameData.id;
        })
        // 未投稿なら投稿済みリストに追加する
        if(isMatch) {
          postedWWADataList.push(gameData);
        }
        return isMatch;
    });
    // これ以降の投稿処理は省略
}
```

これで実行時に未投稿WWAをDiscordに通知する仕組みが出来ましたので、定期実行出来るようにしていきます。
`node-cron` の使い方は実に単純でして、 `cron.schedule` 関数を使い第一引数にスケジュールを、第二引数で実行したい関数をいれるだけで勝手に処理をしてくれます。

今回は1時間おきに実行したいので以下の書き方をしました。

```typescript
import cron from 'node-cron';

/** 毎時定期実行 */
cron.schedule('0 0 * * * *', async () => {
  await checkNewWWAAndPostDiscord();
});
```

node-cronの詳しい使い方はこちらの記事が分かりやすいです。

https://qiita.com/n0bisuke/items/66abf6ca1c12f495aa04

### TypeScriptの実行環境
TypeScriptはそのままでは実行できないので本来はコンパイルする必要がありますが、今回は [ts-node](https://www.npmjs.com/package/ts-node) を使って直接実行します。

TypeScriptと合わせて実行環境作成は以下のコマンドで

```sh
npm install --save typescript ts-node
```

実行するときには以下のように打ち込みます。

```sh
npx ts-node index.ts
```

これで1時間おきにチェックして実行してくれる機構が出来上がりました。

## VPS上の実行環境構築
Local環境で動かすと24時間365日ずっとPCを動かし続けないといけなくなるので、個人で借りているVPSサーバ上で実行をしていきます。

VPSサーバに直接NodeやらTSやらをinstallしてもいいのですが、今回は環境に左右されず決められたバージョンのNodeが利用したいのと、自分のVPS上ではどのアプリが動いてるのか分かりやすくするため原則コンテナ実行するようにマイルールを設けてるのでDockerコンテナ上で動かしていきます。

`Dockerfile` やら `docker-compose.yml` を一から書くのは面倒なので、ChatGPTにポイッと聞いて雛形を出してもらって、若干微調整したりします。

![FireShot Screen Capture #348 - 'Program' - chatgpt.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179706/52bcfc2a-fc2c-9911-ce88-da277baf567f.png)

こうして出来上がった `Dockerfile` と `docker-compose.yml` はご覧の通り
後はVPSサーバ上でコンテナを立ち上げれば完了です。

```Dockerfile
# ベースイメージとして node:20.10-alpine を使用
FROM node:20.10-alpine

# 作業ディレクトリを設定
WORKDIR /app

# package.jsonとpackage-lock.jsonをコピーして依存関係をインストール
COPY package*.json ./
RUN npm install

# アプリケーションファイル全体をコピー
COPY . .
```

```yml
version: '3.1'

services:
  node:
    build: .
    container_name: typescript_app
    volumes:
      - .:/app
      - /app/node_modules # ホストのnode_modulesを隔離するため
    working_dir: /app
    command: ["npx", "ts-node", "index.ts"]
```

これにて完成です。

VPSサーバを持ってなくてもGoogle App Script(GAS)を使ってゴリ押しする方法もありますのでお金がなくても実装できたりしますが、書き方が独特で管理がしにくいので今回は普通のNode+TSで実行する方法を選択しました。

自分の借りてる [indigo](https://web.arena.ne.jp/indigo/) なら月額500円以内でメモリ1GBのVPSが借りられます。...なんか回し者みたいですねｗ

最後にログを確認してみるとご覧の通り
ちゃんと1時間おきに定期実行してますし、データが有る時も投稿されてることが分かります。

![hirarira@pollux_ ~ 2024_11_09 13_28_59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179706/48630f52-276c-d200-f539-3fd91c5645c1.png)

こうして投稿されたのが冒頭で紹介したこちらになります。

![#wwa-plicy新作通知 _ World Wide Adventure - Discord 2024_11_09 4_14_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179706/92d127ef-524c-12ab-1d6b-67a0901afb9c.png)

余談ですがアイコンの「ラーン」は去年のWWAアドベントカレンダーで作ったWWA Script紹介動画にも出てきたオリキャラになります！

![CharaStudio-2024-02-05-02-50-30-Render.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179706/dff771a0-c645-0683-ac7e-ade60c057cdc.png)

https://www.youtube.com/watch?v=UEWztrzxcLc

...と言ったところで余談にそれまくりでしたが、この記事はここまでにしたいと思います。

明日の[WWA Advent Calendar 2024](https://adventar.org/calendars/10185)はアルクスさんの「WWA Script入門：コピペ可、サンプル配布あり！色々やってみよう」になります。

WWAScriptは導入されたばかりで、まだ参入障壁が高いかも知れませんが気になっている！と思っている方もこれを期にWWAScriptの世界に挑戦してくれると嬉しいですね！

それではまた明日の記事をお楽しみに～