5️⃣

Slack-Cloudflare-Workers で Slack に匿名チャンネルを作った

2023/09/19に公開

開発動機

Slack で日々業務のやり取りをしていますが「匿名で会話したい」という要望がメンバーから上がったため、最近話題の Cloudflare Workers と slack-cloudflare-workers を使って Slack 上で匿名チャットができるアプリを作ってみました
元ネタはこちらです: https://qiita.com/peisuke/items/80984db8b47cd8243019

slack-cloudflare-workers とは?

https://zenn.dev/seratch/articles/c370cf8de7f9f5
作者様がこちらの記事に丁寧にまとめてくれています
シンプルかつわかりやすい作りで、今回のアプリも簡単に作れました

完成したもの

https://github.com/sugawani/slack-5ch
使い方は README を読んでみてください
Durable Objects を使っているので Paid プラン推奨ですが、無料プランでも使える方法も書いてあります

実際のメッセージはこんな感じで、日々さまざまなやり取りが匿名で行なわれています

以下はコードの解説です

フローチャート


Slack 側から /5ch,/vip のスラッシュコマンドで送信されたメッセージがトリガーです
Workers 内で実行している処理は以下になります

  • Durable Objects で管理している書き込み番号を取得+インクリメント
  • D1 Database に書き込みログをインサート
  • Workers が受け取ったメッセージを匿名化して指定したチャンネルへ送信

実装詳細

Workers

ID の生成

const makeID = (): string => {
  return `${Math.random().toString(36).slice(2, 8)}0`;
};

本来の 5ch であれば日替わりで ID を生成していますが、弊社は人数が少なく誰が書き込んだか透けてしまうため意図的にすべての書き込みで異なる ID を生成しています
結果的に色んな人が書き込んでいるように見えて擬似的な盛り上がりが作れているかな?と思うので、人数が多い環境でも ID はバラバラの方が良いかもしれません
末尾0に気づいたあなたは友だちになりましょう

名前の生成

const getUsername = (payload: SlashCommand): string => {
  if (payload.text.includes("fusianasan")) {
    return payload.user_id;
  }
  return "名無しさん";
};

基本的には名無しさんです
特に意味はないですがメッセージに fusianasan が含まれる場合にちょっとした遊びを仕込んでます

複数スラッシュコマンドへの対応

const commandRegex = /\/(5ch|vip)/;
app.command(commandRegex, async ({ context, payload }) => {

app.command() の第1引数が文字列と正規表現を受け取れるため正規表現で /vip,/5ch を定義していますが、処理を共通化して /5ch/vip それぞれで app.command() を定義しても良かったかもしれません

空メッセージへの対応

app.command(commandRegex, async ({ context, payload }) => {
  if (!payload.text) {
    return "message は必須です";
  }

/vip のようにメッセージを入力せずに送信してしまうと failed with the error "dispatch_unknown_error" のエラーが発生するため、メッセージが空の場合のバリデーションを行なっています
また、詳細は不明なのですが定期的に slack 側から URL の検証のリクエスト?が飛んできているので、このリクエストによる意図しない ID のインクリメントが発生するのを防ぐ意味合いもあります

Durable Objects

export class IDCounter {
    constructor(private readonly state: DurableObjectState) {}
    async fetch(_request: Request): Promise<Response> {
        const id: number = await this.state.storage?.get("id") || 0;
        await this.state.storage?.put("id", id+1)

        return new Response(id.toString());
    }   
}

Durable Objects から取得、インクリメントして保存しているだけのシンプルな処理です
元々は無料枠で済ませようと思っていたので KV を使用して ID 管理していましたが、せっかくなので課金して強整合の Durable Object で実装してみました
仮に ID がズレても見た目上違和感があるだけなので、正直 KV でも良かったかもしれません

D1 Database

CREATE TABLE IF NOT EXISTS post_messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    res_id INTEGER NOT NULL,
    user_id TEXT NOT NULL,
    text TEXT NOT NULL,
    created_at TEXT
)
const insertPostMessage = async (db: D1Database, payload: SlashCommand, res_id: number): Promise<void> => {
  const now = (new Date()).toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" });
  const result = await db.prepare("INSERT INTO post_messages (res_id, user_id, text, created_at) values (?, ?, ?, ?)").
    bind(res_id, payload.user_id, payload.text, now).
    run();
  if (!result.success) {
    console.error(result.error);
  }
}

D1 Database には書き込まれたメッセージのログを保存しています
データを確認することは基本無いですが、匿名チャットの性質上荒れやすいので念のためのログとして保存するようにしています
シンプルな1クエリのみなので ORM は使用せずに素の API を叩いています

Slack App

display_information:
  name: slack-5ch
  description: slack でやるお!
  background_color: "#000000"
features:
  bot_user:
    display_name: slack-5ch
    always_online: true
  slash_commands:
    - command: /5ch
      url: https://your-cloudflare-workers-url
      description: 匿名でチャットするお!
      usage_hint: キタ━━━━(゚∀゚)━━━━!!
      should_escape: false
    - command: /vip
      url: https://your-cloudflare-workers-url
      description: VIP でやるお!
      usage_hint: おっおっお(^ω^)
      should_escape: false
oauth_config:
  scopes:
    bot:
      - chat:write
      - chat:write.public
      - commands
      - chat:write.customize

Slack App の Manifest はこのような定義になっています
ポイントは chat:write.customize で、書き込み時にアプリのユーザ名を変更するためにはこの権限が必要になります

ローカルでの実行

基本的には冒頭で紹介したこちらの記事を参考に動かしていただければ大丈夫です
開発時には既存の /5ch,/vip のスラッシュコマンドに影響が出ないように /5ch-dev のような開発用のコマンドを作成して、URL だけ開発用に差し替えるようにすると開発がしやすいと思います

その他

別件ですが KV, Durable Objects を使おうとして盛大にハマったので別途記事でまとめました
同じ悩みをお持ちの方の参考になれば幸いです
https://zenn.dev/egstock_inc/articles/95aa6a97caf39a
https://zenn.dev/egstock_inc/articles/1dd8cc2d38f2ef

EGSTOCK,Inc.

Discussion