White Box技術部

WEB開発のあれこれ(と何か)

Vercelを使ってNext.jsアプリでSlackのメッセージをDiscordに配信する

これは、

「Slackの無料アカウントだと90日前のログが見えなくなってしまうから、とりあえずDiscordに転送しておいて見れるようにだけはしておきたいよね」っていうコミュニティ運営問題先送りマンが、 「まあメッセージ転送アプリならboltとdiscord.js使えばサクッといけるっしょ!Vercelの無料枠で大丈夫そうだし」 という感じで作業したら8時間もかかってしまったよ。

という話です。

改めて文章にするとホントSlackにお金払うかDiscord使おうよって話ですね・・・

1. コード作成

実装方針

軽く調査した結果、boltでメッセージをサブスクライブして、discord.jsでWebhookを使ったメッセージ送信をするとサクッとできそうだったので、boltでアプリを作ってどこかでホスティングするのが一番簡単そうでした。

そこでお世話になりがちなVercelでなんか良い感じにできないかなと調べたら、公式にUsing Express.js with Vercelといういけそうなページがあり読んでみたのですが、「サーバレスファンクションとしてなら使えるよ」みたいな内容で、boltアプリを起動させておくのは(昔ながらのserver.jsを使えばいける気もしますが)実質無理っぽかったので、せっかくならとNext.jsのAPIで動作させるコードを目指してみました。

処理コード

もろもろのハマりを解消したあとにコードを見ると単純で、以下のことをしているだけです。

  1. Slackからのリクエストを解析し、メッセージ送信かをチェック
  2. そうであればslack/boltでSlackのユーザ情報を取得し、メッセージを準備
  3. discord.jsでメッセージ送信

最初のurl_verification分岐は、後述のRequest URLのchallengeクリアに必要です。

  • /app/api/events/route.ts
import { App } from '@slack/bolt';
import { WebhookClient } from 'discord.js';
import { NextResponse } from 'next/server';

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
});
const webhookClient = new WebhookClient({ url: process.env.DISCORD_WEBHOOK_URL || '' });

export async function POST(request: Request, response: Response) {
  const body = await request.json();
  const type = body.type;

  if (type === 'url_verification') {
    console.log('req body challenge is:', body.challenge);
    return NextResponse.json({ challenge: body.challenge });
  } else if (type === 'event_callback' && body.event.type === 'message') {
    let username: string | undefined = 'slackbot';
    let avatarURL: string | undefined = undefined;
    if (body.event.user) {
      const result = await app.client.users.info({
        user: body.event.user,
      });
      username = result.user?.real_name;
      avatarURL = result.user?.profile?.image_512;
    }
    const fileInfo = body.files ? '(ファイル添付あり)' : '';

    webhookClient.send({
      content: (body.event.text || '') + fileInfo,
      username: username,
      avatarURL: avatarURL,
    });
  }

  return NextResponse.json({ status: 'ok' });
}

ちゃんとやるのであれば、SlackとDiscordのクライアントはglobalになければ作る系の処理を入れ、エラーハンドリングを入れると良いと思います。

next.config.jsの変更

discord.jsでのメッセージ送信にzlib-syncが必要だったので、ライブラリを追加したのですが、それだけだと以下のようなエラーになります。

Import trace for requested module:
./node_modules/zlib-sync/build/Release/zlib_sync.node
./node_modules/zlib-sync/index.js
./node_modules/discord.js/src/client/websocket/WebSocketManager.js
./node_modules/discord.js/src/index.js
./src/app/api/events/route.ts
 ⨯ ./node_modules/zlib-sync/build/Release/zlib_sync.node
Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
(Source code omitted for this binary file)

これはライブラリにbrowserify-zlibを追加し、webpackの設定をnext.config.jsに追加することで解消しました。

/** @type {import('next').NextConfig} */
const nextConfig = {
  poweredByHeader: false,
  reactStrictMode: true,
  swcMinify: true,
  webpack: (config) => {
    config.resolve = {
      ...config.resolve,
      alias: {
        ...config.resolve.alias,
        "zlib-sync": require.resolve('browserify-zlib'),
      }
    };
    return config;
  },
};

module.exports = nextConfig;

2. Slack側の設定

Slackでアプリを作成します。

作成したらBotを作成し、以下の権限を付与してワークスペースにインストールします。

インストール後は以下の値をメモし、Nextアプリケーションの環境変数に設定します

  • App CredentialsのSigning Secret
    • SLACK_SIGNING_SECRETに設定
  • OAuth & PermissionsのBot User OAuth Token
    • SLACK_BOT_TOKENに設定

Request URLを設定

SlackからのイベントをNextアプリケーションで受け取るために、Event SubscriptionsでEventsを有効にし、Request URLを設定する必要があります。 Request URLはhttpsの必要があるので、ローカルでテストする場合はngrokを利用してnextアプリに転送するのが楽でした。

ngrokの設定手順は省略しますが、設定後にNextアプリケーションを起動し、ngrokを起動して、インターネットからローカルのNextアプリケーションにアクセスできるようにします。

ターミナル1(next)
$ yarn dev

ターミナル2(ngrok)
$ ngrok http http://localhost:3000

ngrokが起動するとhttpsのngrokドメインがコンソールに表示されるので、それに/api/eventsを追加したURLをRequest URLに設定し、challengeをクリアさせます。

※このURLはVercelデプロイ後に再度変更します

Slack動作確認

ここで一度Slackアプリ側に戻り、メッセージを転送したいチャンネルに先ほどインストールしたアプリを招待します。 その後メッセージを送信し、Nextアプリケーションが反応すれば成功です。 ただし、Discordの設定をまだ行っていないため、Discord送信には失敗します。

3. Discord側の設定

Discordアプリを開き、メッセージを転送したいサーバのServer Settingsを開きます。

Webhookの取得

IntegrationsのWebhooksから、Slackのメッセージを転送したいDiscordチャンネルのWebhookを作成します。

  • DISCORD_WEBHOOK_URLに設定する

Botの作成

BotはDiscordアプリからは作成できないので、開発サイトに移動します。

New Applicationから新しくアプリケーションを作成し、SETTINGSのBotからボットを作成します。 IconとUsernameを設定するだけでOKです。

ボットの権限

次にSETTINGSのOAuth2からOAuth2 URL Generatorを選択し、 SCOPESのbotにチェックを付け、BOT PERMISSIONSに以下を設定します。

  • 最低限必要なもの
    • Send Messages
  • 付けたけどどうなんだろってもの
    • Send Messages in Threads
    • Manage Message
    • Embed Links
    • Attach Files

設定後はGENERATED URLのURLにブラウザでアクセスし、ボットを利用するサーバに追加してください。

ここまでやると、SlackのメッセージがDiscordにも送信されるようになります。

4. Vercelの設定

最後にVercelにデプロイしてしまいましょう。 コードをGitHubにpushしたら、Vercelのアカウントから当該リポジトリを選択し、起動させます。 ビルドオプション等はデフォルトで構いませんが、環境変数にこれまで出てきた以下の3つを指定します。

  • SLACK_SIGNING_SECRET
  • SLACK_BOT_TOKEN
  • DISCORD_WEBHOOK_URL

デプロイ後はVercelから与えられたドメインに、SlackのEvent SubscriptionのRequest URLを変更して完了です。

まとめ

コードの全量はこちらです。

とまあ頑張ればNext.jsでもできましたが、typeのチェック処理などはコード補完が効きませんし、Socket modeも使えないので、素直にRender等のホスティングサービスを使ってboltのappをstartさせるのが良いかと思います。

落ち穂拾い

いろいろ試しているときに@discordjs/restを使ったメッセージ送信もやってみたので、サンプルを残しておきます。

まず、Discordのユーザ設定から開発者モードを有効にし、送信対象のDiscordチャンネルIDを取得します。
また、開発サイトで作成したBotのTokenを取得し、アプリケーションのDISCORD_TOKENに設定します。

  • ライブラリを追加
yarn add -E @discordjs/rest discord-api-types
  • コード変更
import { App } from '@slack/bolt';
import { NextResponse } from 'next/server';
import { REST } from '@discordjs/rest';
import { Routes } from 'discord-api-types/v10';

const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN || '');

export async function POST(request: Request, response: Response) {
    // ...省略...
    const fileInfo = body.files ? '(ファイル添付あり)' : ''; // POST内はここまでdiscord.jsと同じ

    await rest.post(Routes.channelMessages('送信先チャンネルID'), {
      body: {
        content: (body.event.text || '') + fileInfo,
      },
    });
  }
  return NextResponse.json({ status: 'ok' });

こっちだとzlib-syncなしでメッセージ送信をすることができますが、Discordにメッセージを送信するBotの情報を変更できないため、今回は利用しませんでした。

そしてあのエラーと戦いました・・・