Slack-Cloudflare-Workers で Slack に匿名チャンネルを作った
開発動機
Slack で日々業務のやり取りをしていますが「匿名で会話したい」という要望がメンバーから上がったため、最近話題の Cloudflare Workers と slack-cloudflare-workers を使って Slack 上で匿名チャットができるアプリを作ってみました
元ネタはこちらです: https://qiita.com/peisuke/items/80984db8b47cd8243019
slack-cloudflare-workers とは?
シンプルかつわかりやすい作りで、今回のアプリも簡単に作れました
完成したもの
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 を使おうとして盛大にハマったので別途記事でまとめました
同じ悩みをお持ちの方の参考になれば幸いです
Discussion