슬랙의 SocketMode, Webhook 을 활용해서 특정 이벤트 발생에 대해 커스텀 콜백을 반환하도록 했다.
본 프로젝트에선 사용자가 스터디 개설을 요청하는 이벤트가 발생할때 이후 슬랙에 전송된 스레드상에서 어떤 버튼을 클릭하는지에 따라 다른 콜백함수를 실행하도록 하여 어드민의 기능을 (스터디 개설 허용 판단) 슬랙봇을 통해 구현했다.
slack api 에 접속해보면 Your Apps 를 통해 앱을 새로 생성해줄 수 있다.
From scratch 클릭
App Name 을 지정하고 설치할 슬랙의 워크스페이스를 선택하여 앱을 생성한다.
interactive features 를 사용하여 슬랙에 전송한 메세지에서 action 을 인식하기 위해 bolt.js 와 여러 토큰들을 생성해야 한다.
bolt.js 라이브러리는 기존에 action, webapi, eventapi... 등 슬랙 API 가 분산되어 있던 것을 하나의 라이브러리에서 모두 제어할 수 있도록 만들어진 라이브러리다.
bolt.js 는 JavaScript, Python, Java SDK 를 제공한다.
npm install @slack/bolt
기본 서버 설정은 다음과 같다.
const { App } = require('@slack/bolt');
// Initializes your app with your bot token and signing secret
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET
});
(async () => {
// Start your app
await app.start(process.env.PORT || 3000);
console.log('⚡️ Bolt app is running!');
})();
그런데 이번 프로젝트는 express 를 사용하고 있다. 그래서 bolt.js 를 통해서도 서버를 열어줄 수 있으나 http, express, bolt.js 를 동시에 사용하기 위해서 다음과 같이 코드를 구성했다.
// slack.js
const { App, LogLevel } = require("@slack/bolt");
const { Study } = require("./models");
let logLevel = LogLevel.INFO;
const {
SLACK_BOT_TOKEN: token,
SLACK_APP_TOKEN: appToken,
SLACK_SIGNING_SECRET: signingSecret,
} = process.env;
const boltApp = new App({
token,
logLevel,
signingSecret,
socketMode: true,
appToken,
});
// ... action 구현
(async () => {
const port = 3000;
// Start your app
await boltApp.start(process.env.PORT || port);
console.log(`⚡️ Slack Bolt app is running on port ${port}!`);
})();
module.exports = boltApp;
먼저 slack 관련 진입점 설정을 위한 slack.js 코드를 작성한다. 이때 슬랙을 위한 서버도 돌아가야 해서 3000번 포트를 사용하여 bolt 서버를 즉시 돌려준다. 여기에 boltApp 을 통해 action 들을 설정하여 slack 에서 버튼을 입력하거나 slash command 를 입력하거나 혹은 shortcut 등의 이벤트가 발생하는 특정 이벤트들에 대해 어떤 콜백을 반환할지 지정할 수 있다.
// index.js
const http = require("http");
const express = require("express");
const app = express();
const boltApp = require("./slack");
const server = http.createServer(app);
// ... router 및 static 등 설정
app.use("/slack/events", (req, res) => {
boltApp.receiver.requestListener(req, res);
});
db.sequelize.sync({ force: false }).then(async () => {
server.listen(PORT, () => {
console.log(`http://localhost:${PORT}`);
});
});
그러고 express 와 http 사용하는 루트 파일인 index.js 에서 boltApp 을 require 로 가져와서 router 만 설정한다.
// CStudy.js
// ...
await boltApp.client.chat.postMessage({
token: process.env.SLACK_BOT_TOKEN,
channel: process.env.CHANNEL_ID,
blocks,
text: "스터디 개설 요청",
});
위의 코드는 controller 에서 특정 API 를 실행시 슬랙에 알림 스레드를 전송하기 위한 코드이다.
저 blocks 는 다음 코드를 통해 만들어줬는데 형태는 slack 의 block-kit 을 활용했다.
function definePayload(
memTotal,
startDate,
endDate,
title,
intro,
category,
studyId,
link
) {
return {
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "스터디 개설이 요청되었습니다.",
},
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `*타이틀:*\n${title}`,
},
{
type: "mrkdwn",
text: `*링크:*\n${link}`,
},
{
type: "mrkdwn",
text: `*시작일:*\n${startDate}`,
},
{
type: "mrkdwn",
text: `*종료일:*\n${endDate}`,
},
{
type: "mrkdwn",
text: `*정원:*\n${memTotal}명`,
},
{
type: "mrkdwn",
text: `*카테고리:*\n${category}`,
},
],
},
{
type: "section",
text: {
type: "plain_text",
text: `소개글:\n${intro}`,
emoji: true,
},
},
{
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
text: "승낙하기",
},
action_id: "approve",
style: "primary",
value: `${studyId}`,
},
{
type: "button",
text: {
type: "plain_text",
text: "거절하기",
},
action_id: "reject",
style: "danger",
value: `${studyId}`,
},
],
},
],
};
}
module.exports = definePayload;
정리
1. slack entry point 생성 및 bolt 서버 가동
2. express 와 bolt 서버 동시에 실행
3. index.js 에 slack receiver 및 router 등록
4. controller 에 특정 이벤트 발생시 slack 으로 소켓 전송
5. 슬랙에서 웹훅을 통해 특정 action 실행시 slack.js 에 설정해둔 커스텀 콜백을 실행
이 과정에서 SLACK_BOT_TOKEN
, CHANNEL_ID
, SLACK_APP_TOKEN
, SLACK_SIGNING_SECRET
값이 필요하다.
이 페이지에선 Incoming Webhooks, Event Subscriptions, Bots, Permissions 설정이 필요하고 하단에서
SLACK_SIGNING_SECRET
를 얻을 수 있다.
Incoming Webhooks
Event Subscriptions
Enable Events 를 토글 On 시켜주고 Subscribe to bot events 에서 message.im 이벤트를 추가한다.
앱의 display name, default name 을 지정한다.
Permissions 는 다음 과정을 거친 이후에 확인하는 편이 좋다.
SLACK_APP_TOKEN
Basic Information 에서 아래로 스크롤하다보면 App-Level Tokens 가 나온다. 여기서 바로 Generate Token and Scopes 를 해서 SLACK_APP_TOKEN
를 생성해도 좋고 아니면
Settings - Socket Mode 로 들어가서 Connect using Socket Mode 에서 Enable Socket Mode 를 On 으로 변환후 하단에 3가지 Features 를 추가한다.
이때 App-Level Token 을 생성하라는 화면이 나왔던 것 같다. 그래서 Basic Information 에서 직접 SLACK_APP_TOKEN
를 만들거나 Socket Mode 를 켜면서 SLACK_APP_TOKEN
를 생성해도 된다.
SLACK_BOT_TOKEN
다시 Permissions 를 클릭하면 Features 에 OAuth & Permissions 로 화면이 전환된다.
여기서 SLACK_BOT_TOKEN
를 획득할 수 있다.
만약 APP_LEVEL Token 이 생성되어 있지 않다면 위의 페이지에서 Bot User OAuth Token 만 보일 것이다.
슬랙은 토큰 종류에 따라 앞에 붙는 prefix 가 다르다.
SLACK_BOT_TOKEN
: xoxp-@@@
SLACK_APP_TOKEN
: xapp-@@@
OAuth & Permissions 에서 아래로 스크롤하다보면 Scopes 설정하는 부분을 볼 수 있다.
여기서 슬랙봇이 할 수 있는 범위를 지정할 수 있다.
Bot Token Scopes
User Token Scopes
마지막으로 Interactivity 토글을 On 시켜줘야 한다.
이게 On 상태로 토글되어야 슬랙에서 버튼을 클릭할때 서버에서 웹훅을 통해 커스텀 콜백을 실행시킬 수 있다.
사실 아직 구하지 않은게 있다. 바로 CHANNEL_ID
슬랙 채널 아이디인데 이건 슬랙에서 보이는 채널 ID 와 다르다.
채널 클릭할때 뜨는 위의 Channel ID 가 아니다.
먼저 슬랙봇을 생성했으면 슬랙에서 알림을 전달하고자 하는 채널을 생성하고 생성한 슬랙봇을 선택한다.
그러고 나면 특정 채널에 슬랙봇을 추가해준다.
이후 slack api 의 conversation.list 를 통해 특정 슬랙봇이 포함되어 있는 슬랙 채널의 ID 를 얻을 수 있다.
여기서 Tester 클릭, provide your own token 에 SLACK_BOT_TOKEN
을 넣고
아래에 비밀 채널로 만들어서 types 를 private_channel 로 설정후 Test method 를 실행하면
이렇게 CHANNEL_ID
를 얻을 수 있다.
bolt.js 가 제공하는 웹훅 기능이 다음과 같다.
// Listen for an event from the Events API
app.event(eventType, fn);
// Convenience method to listen to only `message` events using a string or RegExp
app.message([pattern ,] fn);
// Listen for an action from a Block Kit element (buttons, select menus, date pickers, etc)
app.action(actionId, fn);
// Listen for dialog submissions
app.action({ callback_id: callbackId }, fn);
// Listen for a global or message shortcuts
app.shortcut(callbackId, fn);
// Listen for slash commands
app.command(commandName, fn);
// Listen for view_submission modal events
app.view(callbackId, fn);
// Listen for options requests (from select menus with an external data source)
app.options(actionId, fn);
이중엔 서버에서 슬랙으로 스레드를 보내고 슬랙에서 버튼을 클릭하는 action 을 인지하고 콜백을 실행하고 싶어서 action 을 사용했다.
// slack.js
boltApp.action("approve", async ({ ack, body }) => {
await ack();
body.actions.forEach(async (action) => {
const id = action.value;
const beforeStudyStatus = (await Study.findByPk(id)).status;
try {
const res = await Study.update(
{
status: "ALLOWED",
},
{ where: { id } }
);
const afterStudyStatus = (await Study.findByPk(id)).status;
if (beforeStudyStatus !== afterStudyStatus)
postMessage("스터디 개설을 승낙합니다.", body);
} catch (error) {
console.error(error);
}
});
});
코드가 위와 같은데 action 의 첫 파라미터가 action_id 이다. 이는 위에서 slack block kit 을 통해 blocks 를 어떻게 구성했다고 한 부분중 action 에 넣었고 어떤 action 을 실행할때 어떤 studyid 에 대해 상태를 변경하도록 하고 싶어서 action.value 를 통해 studyId 를 가져올 수 있었다.
이렇게 슬랙봇 전체 시스템을 만들었다.
docs
slack block kit builder
bolt.js github
slack api
slack api tutorial ⭐️ -> 다양한 사례의 코드를 확인할 수 있어서 매우 유용하다.
reference
Log Rocket <Build a Slackbot in Node.js with Slack’s Bolt API>
blog
Kurly Tech Blog - slack block kit 활용