はじめに
こんにちは。
今回は、Slack に新しい絵文字が追加された際に、自動で絵文字追加の通知を行うチャンネルを作成しました。
作成の経緯
最近人が増えてきたり、一緒に仕事する人が増えてきたりで、直接コミュニケーションをする機会が減ってきていると思っています。
なので、テキストコミュニケーションで気軽にリアクションが出来るようにしたいという思いで、今回通知チャンネルを作成するにいたりました。
加えて、便利な絵文字されたら自分も使いたいですからね。
今回やること
- 通知チャンネルを作る
- Slack App を作る
- AWS Lambda + API Gateway を作る
とっても簡単です。
最初 GAS で書けないかなぁと思っていたんですが、エンドポイントのアクセス制限を考えたときに、Slack が API をコール出来なさそうだったのでやめました。(Google WorkSpaceを使ってる場合ドメイン以外からのアクセスの選択肢がパブリックしかないため)→ 有識者いたら教えていただきたいです。。
もう少し詳しくフローを書くと
- Slackに絵文字が追加される
- 絵文字作成のイベントがトリガーされる
- Slack APIがエンドポイントのAPIをコールする
- APIがトリガーされ、Lambdaが実行される
- Lambda関数内で、Slackに通知するためのWebHookがトリガーされる
そのまえに: Slack ワークフローについて
Slack のワークフローを使えばすぐじゃん!って思った方、そうなんです。すぐ出来ます。(実際にやってすぐ出来ました)
ただ、SlackのCLIを使うためにDeno をローカルに追加したくないなぁとか思いからじゃあ AWS でやるかぁとなりました。
Slack の中の人が公開してくれているので、こちらが参考になります。→ Slack で #new-emojis 通知を実現する無料ワークフロー
手順
通知用チャンネルを作成
通知用のチャンネルを作成します。notify_new_emoji
にしました。
Slack のアプリの作成・設定
まず、Slack のアプリを作成する必要があります。以下のリンクから、アプリを作成します。
Slack API: Applications
-
アプリの作成: 「Create New App」ボタンをクリックし、アプリの名前とワークスペースを選択します。
From scratch を選択してください
-
OAuth & Permissions: アプリに必要なスコープを設定します。今回は、絵文字の情報を取得するために
emoji:read
スコープが必要です。 - Signing Secret のメモ: 後ほど認証用に使いますので、Basic Information を選択し、Signing Secret をメモしておきます。
Webhook の設定
次に、Slack の Webhook を設定します。絵文字が追加された際に通知を送信するためのエンドポイントです。
-
Incoming Webhooks の設定
「Incoming Webhooks」タブで Webhooks を有効にし、新しい Webhook URL を作成します。
下までスクロールすると、Add New Webhook to Workspace
とあります。
先ほど作ったチャンネルを指定します。
-
Webhook URL のメモ: 作成した Webhook URL をメモしておきます。
AWS Lambda を作成
今回は同じようなことを何度もやることはないかなと思い、インフラをコード化することは選択しませんでした。
AWS コンソールから Lambda を作っていきます。
- Lambda 関数の作成
関数の作成 → 一から作成を選択し、関数名とランタイムを指定します。今回は Node.js 20.x を選択しました。
- 設定タブから環境変数の追加をします。
メモしておいた、Signinig Secret と Webhook URL をSLACK_SIGNING_SECRET
とSLACK_WEBHOOK_URL
として追加します。 - index.mjs の編集
index.mjs をコンソール上でガリガリ編集していきます。
SAMを使ってローカルで環境でデバッグしながら作っていくでもいいんですが、面倒だったのでログは厚く出すようにしています。
Lambda は実行時 CloudWatch Logs にデフォルトでログが吐き出されるのですが、デバッグしにくいので、テストをするかログをたくさん出すようにするかしたほうがいいと思います。
下記のようなコードを作成しました。
import https from 'https';
import crypto from 'crypto';
// 環境変数
const slackSigningSecret = process.env.SLACK_SIGNING_SECRET;
const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL;
export async function handler(event) {
let response;
try {
console.log('Received event:', JSON.stringify(event, null, 2));
if (!verifySlackRequest(event)) {
console.error('Invalid request signature');
return createResponse(401, { error: 'Invalid request signature' });
}
const body = event.body ? JSON.parse(event.body) : {};
response = await handleEvent(body);
} catch (error) {
console.error('Error processing event:', error);
response = createResponse(500, { error: 'Internal Server Error' });
}
return response;
}
// Signing Secretを使った検証
function verifySlackRequest(event) {
const timestamp = event.headers['X-Slack-Request-Timestamp'];
const slackSignature = event.headers['X-Slack-Signature'];
if (!timestamp || !slackSignature) {
console.error('Missing headers:', { timestamp, slackSignature });
return false;
}
const sigBasestring = `v0:${timestamp}:${event.body}`;
const computedSignature = `v0=${crypto
.createHmac('sha256', slackSigningSecret)
.update(sigBasestring, 'utf8')
.digest('hex')}`;
console.log('Signature base string:', sigBasestring);
console.log('Computed signature:', computedSignature);
return crypto.timingSafeEqual(
Buffer.from(computedSignature, 'utf8'),
Buffer.from(slackSignature, 'utf8')
);
}
async function handleEvent(body) {
if (body.type === 'url_verification') {
return createResponse(200, { challenge: body.challenge });
}
if (body.event?.type === 'emoji_changed' && body.event.subtype === 'add') {
console.log('Emoji added event:', JSON.stringify(body.event, null, 2));
/* eventの中身
"event": {
"type": "emoji_changed",
"subtype": "add",
"name": "hogehoge",
"value": "https://emoji.slack-edge.com/ワークスペースID/name/~~~",
"event_ts": "タイムスタンプ"
},
*/
await sendSlackNotification(body.event.name, body.event.value);
return createResponse(200, { status: 'ok' });
}
console.warn('Ignored event:', JSON.stringify(body, null, 2));
return createResponse(200, { status: 'ignored' });
}
function createResponse(statusCode, body) {
return {
statusCode,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
};
}
async function sendSlackNotification(emojiName, emojiUrl) {
const data = JSON.stringify({
text: `新しい絵文字 :${emojiName}: (${emojiName})が追加されました!${emojiUrl}`,
});
const options = {
hostname: 'hooks.slack.com',
path: new URL(slackWebhookUrl).pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data),
},
};
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let response = '';
res.on('data', (chunk) => {
response += chunk;
});
res.on('end', () => {
console.log('Slack response:', response);
resolve(response);
});
});
req.on('error', (e) => {
console.error('Error sending Slack notification:', e);
reject(e);
});
req.write(data);
req.end();
});
}
API Gateway の作成
- 関数の概要からトリガーを追加する
API Gateway を選択し、HTTP API を作成します。
- 作成されたら詳細から、APIのURLをコピーします
また、Develop > Routesを押下し、パス(Lambdaの関数名と同じ)をコピーし先ほどのURLと合わせます。
https://hogehogehoge.execute-api.region.amazonaws.com/stageName/Lambdaの関数名
SlackのEvent Subscriptionsの設定
- Event SubscriptionsからEnableEventsをして先ほどコピーしたAPIのURLを貼り付けます。
すると、SlackがChallengeというAPIが有効かどうかの検証を行います。 -
Event Subscriptions の設定: API検証が完了したら、絵文字の追加を監視するためにイベントサブスクリプションを設定します。
「Event Subscriptions」タブで「Enable Events」をオンにし、リクエスト URL を設定します。(API を作ってからこちらを設定しますので後述します。) -
Bot のイベントサブスクリプション: SubScribe to bot eventsの項目からemoji_chengedを選択します。
最後にBotアイコンとアプリ名を設定
これは設定するかは好みですが、ChatGPTでそれっぽいの作ってもらいました。
思ったより可愛いのが出てきて満足です。
完成
まとめ
通知チャンネルがずっとあったらいいなと思っていたので、個人的に満足してます。
Slackのワークフローを使えば簡単に出来るので、学習目的以外で今回の手順を行うのはそこまでオススメしません🥺
あと、ChatGPT4oの画像生成機能がイケてるんで、そのうち記事にしようかと思います。