0

Hướng dẫn tạo Slack Bot bằng NestJS

Giới thiệu

Slack Bot là một công cụ hữu ích giúp tự động hóa các tác vụ và tăng cường tương tác trong không gian làm việc Slack. Với khả năng lắng nghe sự kiện, phản hồi tin nhắn, thực thi các lệnh tùy chỉnh (Slash Commands), bot có thể trở thành trợ thủ đắc lực cho đội nhóm của bạn. Trong bài viết này, chúng ta sẽ cùng nhau xây dựng một Slack Bot đơn giản nhưng đầy đủ chức năng cơ bản bằng NestJS, một framework Node.js mạnh mẽ và có cấu trúc rõ ràng. Bot của chúng ta sẽ hỗ trợ Slash Command và lắng nghe một số sự kiện phổ biến từ Slack.

Phần 1: Tạo Slack App và Cấu hình trên Slack

Trước khi viết code, chúng ta cần tạo một ứng dụng trên nền tảng Slack và cấu hình các quyền cũng như điểm cuối (endpoints) cần thiết.

Bước 1: Truy cập Slack API và tạo App

  • Truy cập Slack API Dashboard và đăng nhập vào tài khoản Slack của bạn.
  • Nhấn Create New App để tạo một ứng dụng Slack mới.
  • Chọn From Scratch để tạo ứng dụng từ đầu.
  • Đặt tên App (ví dụ: NestSlackBot) và chọn Workspace nơi bạn muốn cài đặt và thử nghiệm bot này.
  • Nhấn Create App.

Bước 2: Cấu hình quyền (OAuth Scopes)

Quyền (Scopes) xác định những gì bot của bạn được phép làm trong Workspace.

  1. Trong menu bên trái của trang quản lý ứng dụng, chọn OAuth & Permissions.

  2. Kéo xuống phần Scopes. Trong mục Bot Token Scopes, nhấn Add an OAuth Scope.

  3. Thêm các quyền sau:

    • chat:write: Cho phép bot gửi tin nhắn với tư cách là chính nó.
    • commands: Cho phép ứng dụng đăng ký và sử dụng Slash Commands.
    • app_mentions:read: Cho phép bot đọc các tin nhắn có @mention tên bot.
    • channels:join: Cho phép bot tham gia các kênh công khai (public channels). (Lưu ý: Scope này cho phép bot tự tham gia, hữu ích nhưng cần cân nhắc về quyền riêng tư).
    • im:history: Cho phép bot đọc lịch sử tin nhắn trong các cuộc trò chuyện trực tiếp (DM) với người dùng.
    • im:write: Cho phép bot bắt đầu cuộc trò chuyện trực tiếp (DM) với người dùng.
  4. Sau khi thêm đủ scopes, kéo lên đầu trang và nhấn Install to Workspace trong phần OAuth Tokens for Your Workspace.

  5. Xem lại các quyền và nhấn Allow.

  6. Sau khi cài đặt thành công, bạn sẽ thấy Bot User OAuth Token (thường bắt đầu bằng xoxb-...). Hãy sao chép và lưu lại token này một cách an toàn, chúng ta sẽ cần nó ở phần sau (SLACK_BOT_TOKEN).

Bước 3: Kích hoạt lắng nghe Sự kiện (Event Subscriptions)

  1. Vào Event Subscriptions trong menu giao diện quản lý ứng dụng Slack và bật Enable Events.

  2. Nhập Request URL: https://your-server.com/slack/events (sẽ cấu hình sau trong NestJS).

  3. Trong phần Subscribe to bot events, thêm các sự kiện:

    • message.channels: Nhận tin nhắn từ kênh công khai.

    • message.im: Nhận tin nhắn riêng tư.

    • app_mention: Khi bot được nhắc đến trong cuộc trò chuyện.

  4. Nhấn Save Changes để lưu cài đặt.

Bước 4: Bật Slash Command

Chúng ta sẽ tạo một lệnh /demobot đơn giản.

  1. Trong menu bên trái, chọn Slash Commands.

  2. Nhấn Create New Command.

  3. Điền các thông tin sau:

    • Command: /demobot (Đây là lệnh người dùng sẽ gõ trên Slack).
    • Request URL: Nhập URL mà Slack sẽ gửi thông tin khi lệnh này được gọi. Tương tự như Event Subscriptions, định dạng sẽ là https://your-server.com/slack/commands. Chúng ta sẽ cấu hình URL này sau.
    • Short Description: Mô tả ngắn gọn về lệnh (ví dụ: "A simple Demo bot command").
    • (Optional) Usage Hint: Gợi ý cách dùng lệnh (ví dụ: [your text]).
  4. Nhấn Save.

  5. Sau khi lưu, bạn có thể cần cài đặt lại ứng dụng vào Workspace (Reinstall App) để lệnh mới có hiệu lực. Quay lại tab OAuth & Permissions hoặc tìm thông báo yêu cầu cài đặt lại ở đầu trang.

Vậy là chúng ta đã hoàn thành việc cấu hình cơ bản trên Slack. Đừng quên lưu lại Bot User OAuth TokenSigning Secret (lấy từ tab Basic Information -> App Credentials) nhé!

Phần 2: Xây dựng Logic Bot với NestJS

Bây giờ, chúng ta sẽ bắt tay vào viết code cho ứng dụng NestJS.

Bước 1: Cài đặt Project NestJS và Thư viện

Trước tiên, hãy đảm bảo bạn đã cài NestJS CLI. Nếu chưa, chạy lệnh (khuyến nghị dùng yarn hoặc npm):

# Dùng yarn
yarn global add @nestjs/cli
# Hoặc dùng npm
# npm install -g @nestjs/cli

Kiểm tra xem NestJS đã được cài đặt thành công:

nest -v

Tạo một dự án NestJS mới có tên là slack-bot (sử dụng yarn làm trình quản lý package):

nest new slack-bot --package-manager yarn

Chuyển vào thư mục dự án:

cd slack-bot

Cài đặt các package cần thiết:

# Thư viện chính để tương tác với Slack API và Events
yarn add @slack/bolt

# Hỗ trợ đọc biến môi trường từ file .env
yarn add dotenv @nestjs/config

Thử khởi chạy server NestJS:

yarn start:dev

Bạn sẽ thấy thông báo server đang chạy, thường là trên port 3000. Nhấn Ctrl + C để dừng lại.

Bước 2: Thiết lập Biến Môi trường trong NestJS

Để bảo mật các token và thông tin nhạy cảm, chúng ta sẽ sử dụng biến môi trường.

  1. Tạo file .env: Tạo file .env ở thư mục gốc của dự án (slack-bot/.env) với nội dung sau:

    # Lấy từ Basic Information -> App Credentials trong trang quản lý Slack App
    SLACK_SIGNING_SECRET=your_slack_signing_secret
    # Lấy từ OAuth & Permissions -> OAuth Tokens for Your Workspace
    SLACK_BOT_TOKEN=xoxb-your-slack-bot-token
    # (Tùy chọn) Port chạy ứng dụng NestJS
    PORT=3000
    
    • Thay thế your_slack_signing_secret và xoxb-your-slack-bot-token bằng giá trị thực tế bạn đã lấy được ở Bước 2 và từ tab Basic Information trên Slack App.
    • Thêm file .env vào file .gitignore của bạn để tránh vô tình đưa lên Git.
  2. Cấu hình ConfigModule: Mở file src/app.module.ts và cập nhật để NestJS đọc được file .env:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; // Import ConfigModule
import { AppController } from './app.controller';
import { AppService } from './app.service';
// Import SlackModule sẽ tạo ở bước sau
import { SlackModule } from './slack/slack.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    SlackModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Bước 3: Tạo Slack Module và Service

Chúng ta sẽ tạo một module riêng (SlackModule) và một service (SlackService) để quản lý logic liên quan đến Slack, giúp cấu trúc code rõ ràng hơn.

Chạy các lệnh sau trong terminal:

# Tạo module Slack
nest generate module slack

# Tạo service Slack
nest generate service slack

Bây giờ, chúng ta sẽ cấu hình và khởi tạo Slack Bolt App bên trong SlackService. Mở file src/slack/slack.service.ts và cập nhật nội dung:

import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { App as SlackBoltApp, ExpressReceiver } from '@slack/bolt';

@Injectable()
export class SlackService implements OnModuleInit {
  private readonly logger = new Logger(SlackService.name);
  private boltApp: SlackBoltApp;
  private boltReceiver: ExpressReceiver;

  constructor(private readonly configService: ConfigService) {
    this.boltReceiver = new ExpressReceiver({
      signingSecret: this.configService.getOrThrow<string>('SLACK_SIGNING_SECRET'), // Lấy signing secret từ .env
      endpoints: '/slack/events', // Chỉ định endpoint cho events, Bolt sẽ tự xử lý URL verification tại đây
      processBeforeResponse: true, // Nên đặt là true để xử lý logic trước khi gửi response về Slack
    });

    this.boltApp = new SlackBoltApp({
      token: this.configService.getOrThrow<string>('SLACK_BOT_TOKEN'), // Lấy bot token từ .env
      receiver: this.boltReceiver,
      processBeforeResponse: true,
    });
  }

  async onModuleInit() {
    this.registerListeners();
    this.logger.log('Slack Bolt App Initialized and Listeners Registered');
  }

  getReceiver(): ExpressReceiver {
    return this.boltReceiver;
  }

  getBoltApp(): SlackBoltApp {
      return this.boltApp;
  }

  private registerListeners(): void {
    this.logger.log('Registering Slack listeners...');

    this.boltApp.command('/demobot', async ({ command, ack, say, client, logger }) => {
      await ack();
      logger.log(`Received /demobot command from user ${command.user_id}`);

      const userId = command.user_id;
      const textPayload = command.text; // Lấy phần text người dùng nhập sau lệnh

      try {
        await client.chat.postMessage({
          channel: userId, // Để gửi DM, channel chính là User ID
          text: `Chào <@${userId}>! Bạn đã gọi /demobot ${textPayload ? `với nội dung: "${textPayload}"` : ''}. Rất vui được hỗ trợ!`,
        });
        logger.log(`Responded to /demobot command from user ${userId}`);
      } catch (error) {
        logger.error(`Failed to handle /demobot for user ${userId}: ${error.message || error}`);
        try {
            await client.chat.postMessage({
                channel: userId,
                text: `Rất tiếc, đã có lỗi xảy ra khi thực hiện lệnh /demobot.`
            });
        } catch (postError) {
             logger.error(`Failed to send error DM to user ${userId}: ${postError.message || postError}`);
        }
      }
    });

    this.boltApp.event('app_mention', async ({ event, say, logger }) => {
      logger.log(`Received app_mention event from user ${event.user} in channel ${event.channel}`);
      try {
        await say({
            text: `Chào <@${event.user}>, bạn cần giúp gì ạ? :blush:`,
            thread_ts: event.thread_ts || event.ts // Trả lời trong thread nếu có
        });
      } catch (error) {
        logger.error(`Failed to handle app_mention: ${error.message || error}`);
      }
    });

    this.boltApp.message(async ({ message, event, say, logger }) => {
        if (message.channel_type === 'im' && !message.subtype && !event.bot_id) {
             logger.log(`Received direct message from user ${event.user}: "${message.text}"`);
             try {
                await say(`Tôi nhận được tin nhắn của bạn: "${message.text}". Hiện tại tôi chưa xử lý được nhiều :sweat_smile:`);
             } catch (error) {
                 logger.error(`Failed to handle direct message: ${error.message || error}`);
             }
        }
    });

    this.logger.log('Slack listeners registered successfully!');
  }
}

Giải thích chi tiết:

  • Chúng ta dùng ConfigService của NestJS để lấy các biến môi trường một cách an toàn (getOrThrow sẽ báo lỗi nếu biến không tồn tại).
  • ExpressReceiver được cấu hình với signingSecret và endpoints. Việc chỉ định endpoints ở đây giúp Bolt tự động xử lý URL Verification mà Slack yêu cầu khi bạn lưu Request URL trong Event Subscriptions.
  • SlackBoltApp được khởi tạo với token và receiver.
  • onModuleInit() là nơi lý tưởng để gọi registerListeners(), đảm bảo mọi thứ sẵn sàng trước khi bắt đầu lắng nghe.
  • Trong registerListeners():
    • boltApp.command(): Đăng ký xử lý cho Slash Command. Lưu ý việc gọi ack() sớm và cách gửi DM bằng client.chat.postMessage với channel: userId.
    • boltApp.event('app_mention', ...): Đăng ký xử lý khi bot được nhắc đến.
    • boltApp.message(...): Đây là cách khác để lắng nghe sự kiện message. Bolt cung cấp các hàm helper như message() để lắng nghe sự kiện này (thay vì event('message', ...)). Chúng ta thêm điều kiện lọc để chỉ xử lý tin nhắn DM từ người dùng thực.

Bước 4: Tạo Slack Controller

Controller này đóng vai trò cầu nối giữa HTTP requests từ Slack và ExpressReceiver của Bolt. Chạy lệnh sau:

nest generate controller slack

Mở file src/slack/slack.controller.ts và cập nhật:

import { Controller, Post, Req, Res, Logger, Get, HttpCode } from '@nestjs/common';
import { Request, Response } from 'express';
import { SlackService } from './slack.service';
import { ExpressReceiver } from '@slack/bolt';

@Controller('slack')
export class SlackController {
  private readonly logger = new Logger(SlackController.name);
  private receiver: ExpressReceiver;

  constructor(private readonly slackService: SlackService) {
    this.receiver = this.slackService.getReceiver();
  }

  @Post('events')
  async handleEvents(@Req() req: Request, @Res() res: Response) {
    await this.receiver.requestHandler(req, res);
  }

  @Post('commands')
  async handleCommands(@Req() req: Request, @Res() res: Response) {
    await this.receiver.requestHandler(req, res);
  }
}

Giải thích:

  • Controller định nghĩa các route POST /slack/events và POST /slack/commands.
  • Quan trọng nhất là việc gọi this.receiver.requestHandler(req, res) trong mỗi route. Hàm này của Bolt là "trái tim" xử lý mọi request đến từ Slack, đảm bảo tính đúng đắn và định tuyến đến các listeners bạn đã viết trong SlackService.

Bước 5: Chạy và Kiểm tra

  1. Cập nhật Request URL:
  • Bạn cần một URL công khai để Slack có thể gửi request đến ứng dụng NestJS của bạn. Trong quá trình phát triển, bạn có thể sử dụng các công cụ như ngrok hoặc localtunnel.
  • Ví dụ với ngrok: Chạy ngrok http 3000 (nếu app NestJS chạy ở port 3000). Ngrok sẽ cung cấp cho bạn một URL dạnghttps://<random_string>.ngrok.io
  • Quay lại trang quản lý Slack App:
    • Vào Event Subscriptions, cập nhật Request URL thành https://<random_string>.ngrok.io/slack/events. Slack sẽ cố gắng verify URL này, nếu NestJS app đang chạy và receiver được cấu hình đúng, nó sẽ thành công.
    • Vào Slash Commands, chỉnh sửa lệnh /demobot và cập nhật Request URL thành https://<random_string>.ngrok.io/slack/commands.
  • Lưu ý: Mỗi khi khởi động lại ngrok, bạn sẽ có URL mới và cần cập nhật lại trên Slack. Đối với môi trường Staging/Production, bạn sẽ dùng URL công khai thực tế của server.
  1. Khởi chạy ứng dụng NestJS:
yarn start:dev
  1. Kiểm tra trong Slack:
  • Mở Workspace Slack của bạn.
  • Thử Slash Command: Gõ /demobot và nhấn Enter. Bot của bạn (nếu đang chạy và nhận được request từ Slack qua ngrok) sẽ gửi một tin nhắn trực tiếp cho bạn.
  • Thử @mention: Trong một kênh mà bạn đã mời bot vào (hoặc trong App Home của bot), gõ @Tên_Bot_Của_Bạn Chào bot! (ví dụ: @NestSlackBot Chào bot!). Bot sẽ phản hồi lại trong kênh đó.
  • Thử DM: Gửi một tin nhắn trực tiếp cho bot. Bot sẽ echo lại tin nhắn của bạn.
  1. Xem Logs: Theo dõi cửa sổ terminal nơi bạn chạy yarn start:dev để xem các log từ Logger, giúp gỡ lỗi nếu có vấn đề.

Kết luận

Chúc mừng! Bạn đã xây dựng thành công một Slack Bot cơ bản bằng NestJS, có khả năng xử lý Slash Command và các sự kiện như tin nhắn, mention. Từ nền tảng này, bạn có thể mở rộng thêm nhiều tính năng phức tạp hơn như tương tác với các nút bấm, menu, modals, tích hợp với các API khác, lưu trữ dữ liệu, v.v.

NestJS cung cấp một cấu trúc vững chắc để xây dựng các ứng dụng Slack phức tạp, trong khi thư viện @slack/bolt giúp đơn giản hóa việc tương tác với API và sự kiện của Slack.

Hy vọng phần tiếp theo này đáp ứng được yêu cầu của bạn! Chúc bạn xây dựng được những Slack Bot tuyệt vời với NestJS.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí