nove-b

nove-b/PM2を使ってMastodonのBotを作成した

Created Mon, 03 Feb 2025 00:00:00 +0000 Modified Tue, 11 Feb 2025 16:37:11 +0000
1144 Words 5 min

前回に引き続き、ソロインスタンスのローカルタイムラインをRSS Feedとして活用したいという思いで、今回は追加したアカウンをBotとして機能するようにした。

Botをどこで動かすのか

Mastodonを触るうえでディレクトリ構成に悩む。 今回もどこで動かすか迷ったうえ、/home/mastodon/以下に作成することにした。

投稿する仕組みを作成する

import axios from "axios";
import Parser from "rss-parser";
import dotenv from "dotenv";
import fs from "fs";

dotenv.config();

const parser = new Parser();
const MASTODON_API_URL = "https://instanceURL/api/v1/statuses";
const ACCESS_TOKEN = process.env.MASTODON_ACCESS_TOKEN;

const rssUrls = ["https://blog.nove-b.dev/index.xml"];

const POSTED_URLS_FILE = "posted_urls.json";

// 投稿済みURLを読み込む
function loadPostedUrls(): Set<string> {
  try {
    if (!fs.existsSync(POSTED_URLS_FILE)) {
      console.log(
        `File ${POSTED_URLS_FILE} does not exist. Creating a new one.`,
      );
      fs.writeFileSync(POSTED_URLS_FILE, JSON.stringify([], null, 2), "utf8");
      console.log(`${POSTED_URLS_FILE} successfully created.`);
    }
    const data = fs.readFileSync(POSTED_URLS_FILE, "utf8");
    return new Set(JSON.parse(data));
  } catch (error) {
    console.error("Error loading posted URLs:", error);
    return new Set();
  }
}

// 投稿済みURLを保存する
function savePostedUrls(postedUrls: Set<string>) {
  try {
    fs.writeFileSync(
      POSTED_URLS_FILE,
      JSON.stringify([...postedUrls], null, 2),
      "utf8",
    );
    console.log(`Updated ${POSTED_URLS_FILE}`);
  } catch (error) {
    console.error("Error saving posted URLs:", error);
  }
}

async function fetchAndPost() {
  try {
    const postedUrls = loadPostedUrls();

    for (const url of rssUrls) {
      const feed = await parser.parseURL(encodeURI(url));
      if (feed.items.length === 0) {
        console.log(`No items found in RSS feed: ${url}`);
        continue;
      }

      let hasNewPost = false;

      for (const post of feed.items) {
        if (!post.link || postedUrls.has(post.link)) {
          continue;
        }

        const status = `🎉 ${post.title} 🎉\n🔗 ${post.link}`;
        try {
          const response = await axios.post(
            MASTODON_API_URL,
            { status, visibility: "public" },
            {
              headers: {
                Authorization: `Bearer ${ACCESS_TOKEN}`,
                "Content-Type": "application/json",
              },
            },
          );
          console.log(`Successfully posted: ${post.title}`, response.data);
        } catch (error: any) {
          console.error(
            "Error posting to Mastodon:",
            error.response?.data || error.message,
          );
        }
        postedUrls.add(post.link);
        hasNewPost = true;
      }

      if (hasNewPost) {
        savePostedUrls(postedUrls);
      }
    }
  } catch (error) {
    console.error("Error fetching or posting RSS:", error);
  }
}

// 定期実行(30分ごと)
setInterval(fetchAndPost, 30 * 60 * 1000);

// 初回実行
fetchAndPost();

npx tscでビルドして、node dist/index.jsで実行で投稿されることを確認した。

PM2で永続化し、Botとして機能させる

PM2とは?

PM2 は、アプリケーションを 24 時間 365 日オンラインで管理および維持するのに役立つデーモン プロセス マネージャーです。 https://pm2.keymetrics.io/

これで常時、jsを動かしてBot化することができた。

// pm2のインストール
npm install -g pm2
// pm2でBotを起動
pm2 start {path}/index.js --env MASTODON_ACCESS_TOKEN={access_token}
// pm2で起動したBotを自動起動設定
pm2 startup
pm2 save

pm2 listで起動しているか確認することもできる。

┌────┬─────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name                │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├────┼─────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 1  │ rss-mastodon-bot    │ default     │ 1.0.0   │ fork    │ 141470   │ 69s    │ 0    │ online    │ 0%       │ 60.7mb   │ root     │ disabled │

ちなみに削除は、pm2 delete rss-mastodon-botで実行できる。

Chat GPTにお世話になりすぎている

今回もまたChat GPTにいろいろ聞いた。 理解しきれていないので、カスタマイズしつつ理解を深めていきたい。