Slack là một kênh liên lạc được khá nhiều công ty đang sử dụng hiện nay. Slack cung cấp nhiều tính năng, trong đó có hỗ trợ tạo button thao tác, giúp các developer có thể tự build cho mình những con Bot dễ dàng.
Bài viết này sẽ chia sẻ cách tạo con Bot có kèm button trong tin nhắn, khi click vào button, sẽ thực hiện một request đến webhook của server chúng ta, từ đó chúng ta có thể xử lý nghiệp vụ hệ thống mình một cách tự động.

Demo chúng ta sẽ làm như sau:

  1. Gửi 1 tin nhắn đến Slack có kèm button
  2. Click vào button
  3. Hệ thống sẽ bắt tín hiệu từ slack bằng webhook và update message đó với message mới mà không có button
  4. Hệ thống sẽ gửi trả lại thông điệp vào thread của message đó là đã xử lý xong

Bây giờ chúng ta bắt đầu thôi

1. Tạo 1 server bằng nodejs

Mình sẽ tạo một webhook bằng nodejs lắng nghe ở port 50300. Khi user click vào action button trên slack, thì slack sẽ gửi 1 request đến webhook này, từ đây developer có dữ liệu để xử lý tiếp các nghiệp vụ của hệ thống họ.

const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const morgan = require('morgan');

const port = 50300;
const app = express();

app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(morgan('dev'));

app.post('/mySlackWebhook', (req, res) => {
	res.send({
		status: true,
		message: 'POST: hello world',
		data: {}
	});
});

app.listen(port, () => {
	console.log(`Example app listening on port ${port}`)
});

Thử start server lên nào

% node debugPostSlackCloudFunctioBlog.js 
Example app listening on port 50300

Gửi một request với phương thức POST đến webhook

% curl -m 70 -X POST http://localhost:50300/mySlackWebhook | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    55  100    55    0     0  18333      0 --:--:-- --:--:-- --:--:-- 18333
{
  "status": true,
  "message": "POST: hello world",
  "data": {}
}

Vì server đang ở local, nếu đăng ký webhook url là http://localhost:50300/mySlackWebhook cho Slack thì chắc chắn Slack sẽ không hiểu và bạn cũng không nhận được bất kì request nào đến server.
Vì vậy để Slack hiểu đc url ở local, chúng ta sẽ sử dụng 1 ứng dụng khá hay là ngrok. ngrok sẽ hỗ trợ biến đổi server localhost thành một server online mà có thể truy cập trên internet.

Download ngrok tại đây.

Bạn vẫn giữ nguyên trạng thái listening của server nodejs, sử dụng biến đổi localhost với port 50300 thành một url online bằng lệnh sau:

ngrok http 50300

Lúc này ngrok sẽ tạo cho bạn 2 url httphttps

Region                        United States (us)                                                                           
Web Interface                 http://127.0.0.1:4040                                                                        
Forwarding                    http://c8d9-2402-800-63a9-8de8-cde-7a11-a130-db33.ngrok.io -> http://localhost:50300         
Forwarding                    https://c8d9-2402-800-63a9-8de8-cde-7a11-a130-db33.ngrok.io -> http://localhost:50300        

Bây giờ thử sử dụng link mà ngrok tạo và gửi 1 request đến webhook của mình

% curl -m 70 -X POST https://c8d9-2402-800-63a9-8de8-cde-7a11-a130-db33.ngrok.io/mySlackWebhook | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    55  100    55    0     0  18333      0 --:--:-- --:--:-- --:--:-- 18333
{
  "status": true,
  "message": "POST: hello world",
  "data": {}
}

OK, vậy là webhook chúng ta đã chạy ổn và có thể đăng ký webhook này với Slack.

2. Tạo Slack app

Bước này chắc chắn bạn đã có tài khoản slack rồi nhé.
Vào https://api.slack.com/apps để tạo một app.

1Cb-054e58lZB7EoEkue3S5KJRgvpbilL

Chọn tạo app bằng From an app manifest. Nghĩa là mình sẽ định nghĩa app theo 1 scirpt (kịch bản) đã viết sẵn. Kịch bản như sau:

display_information:
  name: Outcoming APP
features:
  bot_user:
    display_name: Outcoming APP
    always_online: false
oauth_config:
  scopes:
    bot:
      - chat:write
settings:
  interactivity:
    is_enabled: true
    request_url: https://c8d9-2402-800-63a9-8de8-cde-7a11-a130-db33.ngrok.io/mySlackWebhook
  org_deploy_enabled: false
  socket_mode_enabled: false
  token_rotation_enabled: false

Copy kịch bản trên, sửa request_url phù hợp với webhook url của bạn và dán vào phần định nghĩa.

  • display_information.name: tên app
  • oauth_config.scopes.bot: để app có quyền send message hay update message thì sẽ set cho quyền chat:write. Để hiểu thêm về quyền tương ứng với api nào thì bạn đọc thêm tài liệu của slack nhé.
  • settings.interactivity.is_enabled: chọn true nhé. Để kích hoạt chức năng tương tác.
  • settings.interactivity.request_url: chính là webhook url của bạn

Sau khi tạo thành công app, bạn cần phải setting app với Workspace

1bNvaTcnLn9dGRmrqogbUz-pwO8lJjNGI

Về cơ bản tới bước này chúng ta đã có 1 app (hay gọi khác hơn là 1 con bot) kết nối với Workspace của bạn. Bây giờ muốn con bot đó hoạt động trong channel nào thì chúng ta add bot vào channel đó.
Tạo hoặc chọn một channel đã có, bạn invite bot đó vào như sau:

/invate @Bot Name

1xMPghdqtlDqvmhAJg0IT2FotXWBvaqXV

Ok, bot đã có mặt trong channel muốn gửi tin nhắn rồi. Bây giờ muốn gửi tin nhắn đến channel này, bạn cần phải có Key của bot. Lấy key ở đây và bỏ vào code nodejs của mình nhé.

14aY1ksaA9mBgc6DvtjvaTWpw7zjqFZn1

3. Gửi 1 tin nhắn đến Slack bằng API

Bây giờ thử gửi 1 tin nhắn đến slack kèm theo button bằng curl thử nhé

curl -m 70 -X POST https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer xoxb-25232XXXXXXXX-35935XXXXXXXX-iKXXXXXXXX" \
-H "Content-Type:application/json" \
-d '{
    "channel": "C03HCXXXXX",
    "text": "<!channel>",
    "attachments": [
        {
            "text": "Hello, Do you want approve this message?",
            "callback_id": "approve123",
            "color": "#3AA3E3",
            "attachment_type": "default",
            "actions": [
                {
                    "name": "outcomingID",
                    "text": "Approve",
                    "type": "button",
                    "style": "primary",
                    "value": "1234"
                }
            ]
        }
    ]
}'

Bạn sẽ thấy channel nhận được một tin nhắn từ bot của bạn

1g0RUgaEURciSLHdCgmRZVhOWKt7wIFWN

Cụ thể những params trên bạn có thể đọc trên tài liệu của Slack API.
Về cơ bản thì mình sử dụng attachments để định nghĩa đoạn tin nhắn, trong đó có kèm một action, action này mình định nghĩa nó là kiểu button, ngoài ra Slack còn hỗ trợ các kiểu khác như select. Nó giống như là tag input trong html vậy.

4. Update tin nhắn

Khi bạn click button Approve trên message của Slack, Slack sẽ gửi đến webhook của bạn 1 request (thông tin webhook bạn đã setup ở step 2 với thông số request_url: https://c8d9-2402-800-63a9-8de8-cde-7a11-a130-db33.ngrok.io/mySlackWebhook).
Dựa vào thông tin của request mà Slack gửi đến, hệ thống của chúng ta sẽ xử lý nghiệp vụ liên quan. Ở demo này mình sẽ update message cũ trên slack thành message mới.

4.1 Setup webhook

const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const morgan = require('morgan');

const port = 50300;
const app = express();

app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(morgan('dev'));

app.post('/mySlackWebhook', (req, res) => {
	const payload = req.body.payload ? JSON.parse(req.body.payload) : '';
	console.log(">>> payload: ", req.body.payload);
});

app.listen(port, () => {
	console.log(`Example app listening on port ${port}`)
})

Thông tin của Slack trả về cho webhook:

# ----- body
{
  "type": "interactive_message",
  "actions": [
    {
      "name": "outcomingID",
      "type": "button",
      "value": "1234"
    }
  ],
  "callback_id": "outcomingID=1234",
  "team": {
    "id": "T02FD6TBH3L",
    "domain": "vnlabcenter"
  },
  "channel": {
    "id": "C03HCJ5EVHT",
    "name": "privategroup"
  },
  "user": {
    "id": "U02FRJ9LWK1",
    "name": "landth"
  },
  "action_ts": "1654247385.489716",
  "message_ts": "1654246293.513859",
  "attachment_id": "1",
  "token": "XUXwdW9BdsknFkvOxbF6bvIq",
  "is_app_unfurl": false,
  "enterprise": null,
  "is_enterprise_install": false,
  "original_message": {
    "bot_id": "B03H8T3CURL",
    "type": "message",
    "text": "<@U02GJU46LQZ>\n<@U02FRJ9LWK1>",
    "user": "U03HFFE9YSX",
    "ts": "1654246293.513859",
    "app_id": "A03J592SQG0",
    "team": "T02FD6TBH3L",
    "bot_profile": {
      "id": "B03H8T3CURL",
      "deleted": false,
      "name": "Outcoming APP",
      "updated": 1653881869,
      "app_id": "A03J592SQG0",
      "icons": {
        "image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",
        "image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",
        "image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png"
      },
      "team_id": "T02FD6TBH3L"
    },
    "attachments": [
      {
        "id": 1,
        "color": "3AA3E3",
        "fallback": "出金データID=<https://example.com|1234>が作成されました",
        "text": "出金データID=<https://example.com|1234>が作成されました",
        "callback_id": "outcomingID=1234",
        "actions": [
          {
            "id": "1",
            "name": "outcomingID",
            "text": "振込依頼",
            "type": "button",
            "value": "1234",
            "style": "primary"
          }
        ]
      }
    ],
    "blocks": [
      {
        "type": "rich_text",
        "block_id": "vmM",
        "elements": [
          {
            "type": "rich_text_section",
            "elements": [
              {
                "type": "user",
                "user_id": "U02GJU46LQZ"
              },
              {
                "type": "text",
                "text": "\n"
              },
              {
                "type": "user",
                "user_id": "U02FRJ9LWK1"
              }
            ]
          }
        ]
      }
    ]
  },
  "response_url": "https://hooks.slack.com/actions/T02FD6TBH3L/3641284386368/9gSKqHo5GbRHvrqgAbi6Qn2E",
  "trigger_id": "3617453522275.2523231391122.0e95b0234d5ebfc73933fdbe299064a0"
}

# ----- header
{
  "host": "bf92-2402-800-63a9-8de8-50e6-95bf-798-df07.ngrok.io",
  "user-agent": "Slackbot 1.0 (+https://api.slack.com/robots)",
  "content-length": "2518",
  "accept": "application/json,*/*",
  "accept-encoding": "gzip,deflate",
  "content-type": "application/x-www-form-urlencoded",
  "x-forwarded-for": "35.175.209.114",
  "x-forwarded-proto": "https",
  "x-slack-request-timestamp": "1654238475",
  "x-slack-signature": "v0=b0cee9a65ba97dce7f95870248a691cdb1bcf0b40066b5ec434d528aa151f54f"
}

4.2 Update message

Từ thông tin mà webhook nhận được, bạn sẽ có được thông tin như:

  • payload.channel.id: ID của channel
  • payload.type: type message
  • payload.response_url: url của slack message, bạn có thể thực hiện update message trên slack ngay lập tức với thông tin url này
const headers = {
    'content-type' : 'application/json',
    'Authorization': 'Bearer xoxb-25232XXXXXXXX-35935XXXXXXXX-iKXXXXXXXX'
};

const responseUrl = payload.response_url ? payload.response_url : '';
const attachments = payload.original_message.attachments;
attachments[0].text = "Your message is approved";
attachments[0].actions = undefined; // xóa button đi, mục đích không cho user click request approve lần nữa

// mình lấy lại thông tin blocks, attachements để giữ lại format hình dáng của message ban đầu. Bạn có thể định nghĩa 1 kiểu message khác tùy ý
const respData = {
    "blocks": payload.original_message.blocks,
    "attachments": attachments
};

request.post({
    headers,
    url: responseUrl,
    json: respData
}, function(error, response, body){
    console.log("response ok: ", body.ok);
});

1JhfgPxLlwhY8IL7VS1Ve8tp5yVQkm5hD

Ngoài cách dùng response_url, bạn cũng có thể update message thông qua API https://slack.com/api/chat.update

curl -m 70 -X POST https://slack.com/api/chat.update \
-H "Authorization: Bearer xoxb-XXXXXXXXX" \
-H "Content-Type:application/json" \
-d '{
	"channel": "ChannelID",
	"ts": "message_ts",
	"text": "Your message is updated"
}'

5. Gửi 1 tin nhắn vào thread

Trường hợp bạn không muốn replace message ban đầu, bạn có thể gửi 1 message mới vào thread của message ban đầu.

const channelID = payload.channel ? payload.channel.id : '';
const ts = payload.message_ts ? payload.message_ts : '';

const threadData = {
    "channel": channelID,
    "thread_ts": ts,
    "text": "This is message in thread"
};

request.post({
    headers,
    url: `https://slack.com/api/chat.postMessage`,
    json: threadData
}, function(error, response, body){
    console.log("sent to thread ok: ", body.ok);
});

1i-Atn15fwB1wQqEys1btr9rYEtW9PIev

6. Thêm verify request từ Slack

Để tăng tính security, bạn có thể thêm đoạn verify chữ kí từ Slack, để xác minh request đến webhook của bạn là từ Slack chính hiệu.

const signVerification = (req) => {
	const slack_signing_secret = 'a7a1223e9d481553c616bc433edf07b9';
	const request_body = qs.stringify(req.body,{ format:'RFC1738' });
	const timestamp = req.headers['x-slack-request-timestamp'];
	const sig_basestring = 'v0:' + timestamp + ':' + request_body;
	const sha256Hasher = crypto.createHmac("sha256", slack_signing_secret);
	const my_signing = 'v0=' + sha256Hasher.update(sig_basestring).digest("hex");
	const slack_signing = req.headers['x-slack-signature'];

	if (crypto.timingSafeEqual(
		Buffer.from(my_signing, 'utf8'),
		Buffer.from(slack_signing, 'utf8'))
	) {
		return true;
	}
	
	return false;
}

app.post('/mySlackWebhook', (req, res) => {
    if (!signVerification(req)) {
        return res.status(400).send('Verification failed');
    }

    // your code
});

7. Kết luận

Slack đã cung cấp khá nhiều api và các loại action hữu ích, từ đó bạn có thể build riêng cho mình 1 application tiện lợi.

8. Tham khảo