ทดลองทำ Slack Slash Command

ทดลองทำ Slack Slash Command Cover Image

ปัจจุบันจะเห็นว่าหลายๆบริษัทได้นำเอา Slack มาใช้ในการคุยงานกันมากขึ้น รวมถึงที่ Nextzy Technologies ก็เช่นกัน เนื่องจากทุกๆวัน ต้องเปิดหน้าจอ Slack ตลอด ก็เลยอยากจะลองเขียน Slash Command ขึ้นมา เพื่อจะได้ไม่ต้องสลับจอบ่อยๆ จึงเกิดเป็นบทความนี้ขึ้นมา นั่นเอง

บทความนี้ผมใช้วิธีเขียน Slash Command โดยฝั่ง Server เขียนด้วย Hapijs และใช้Digital Ocean สำหรับรัน Server

Slack Slash Command คืออะไร ?

Slash Command คือ ชุดคำสั่งที่ให้เราสามารถจะพิมพ์หรือดึงข้อมูลต่างๆผ่านหน้า Slack Chat ได้เลย โดยการพิมพ์ / แล้วตามด้วยคำสั่งที่เราเขียนไว้

หลักการทำงาน

หลักการทำงานของ Slash Command คือ

  1. เมื่อผู้ใช้งานพิมพ์คำสั่ง / slash command มันก็จะถูกส่ง HTTP POST ไปยัง Server ของเรา
  2. Server ทำการ validate ว่าเป็นคำสั่งที่ส่งมาจาก slack จริงหรือไม่ โดยอาจจะใช้token ที่ส่งมา
  3. ถ้าจริงก็ส่งข้อมูลกลับไปที่ slack ตาม format ที่ถูกต้อง

Step 1 : Create Slash Commands Service

เริ่มแรกให้เข้าไปที่ New Slash Commands เพิ่อเปิดใช้งาน Slash Service (จำเป็นที่จะต้องมี permission ในทีมนั้นๆด้วย) หรือจะเข้าไปหาจาก Slack App ก็ได้

Create Slash Service

เมื่อเรากด สร้างเสร็จ ก็จะมาถึงหน้า Integrating Setting ก็ให้เรากำหนด

  • Command : อันนี้เอาไว้กำหนดว่า เราต้องการแบบไหน ของผมใช้ /ahoy
  • URL : (อันนี้ต้องเป็น server จริงๆ ของผมสร้าง Droplet ใน Digital Ocean มาตัวนึง)
  • Method : เราจะให้ส่ง HTTP เป็นชนิดอะไรก็กำหนดไว้
  • Token : อันนี้สำคัญครับ เป็นความลับห้ามให้ใครรู้ ควรทำการ validate ข้อมูลทุกครั้งที่ส่ง

สุดท้ายเรียบร้อยแล้ว เราสามารถใช้คำสั่ง /ahoy ผม Slack ได้แล้ว แต่ว่ายังไม่มีอะไรเกิดขึ้น เนื่องจากเรายังไม่ได้ทำการ implement ฝั่ง Server เลย

Type Ahoy Command

Step 2 : Create Server

ตัว Server ผมใช้ Hapijs ในการทำ สร้างโปรเจ็คด้วย npm init หรือแบบไหนตามสะดวก

ติดตั้ง dependencies เหล่านี้ลงไป

npm install hapi chalk dotenv --save

ส่วนของผมจะได้ไฟล์ package.json หลังจากติดตั้ง dependencies ประมาณนี้

{
  "name": "slack-slash-command",
  "version": "1.0.0",
  "dependencies": {
    "chalk": "^1.1.3",
    "dotenv": "^2.0.0",
    "hapi": "^13.4.0"
  },
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  }
}

เนื่องจากฝั่ง Server เราแค่กำหนด route ที่ต้องการจะใช้ Slack ส่งข้อมูลมาตอนที่เราพิมพ์ command ฉะนั้นทางฝั่ง server ผมก็กำหนดไว้แค่

{
  method: 'POST',
  path: '/ahoy',
  handler: (request, reply) => {
    let payload = request.payload;
  }
}

ซึ่งเป็น path เดียวกันกับที่ผมกำหนด URL ในส่วน Integrating Setting นั่นเอง

หลังจากนั้นผมทำการสร้าง server.js, routes.js, controller.js และ logger.js ขึ้นมา เพื่อให้มัน handle http://MY_WEB_URL/ahoy แบบง่ายๆจาก slack ได้

ไฟล์ server.js

'use strict';

const Hapi = require('hapi');
const server = new Hapi.Server();
const chalk = require('chalk');

const routes = require('./routes');

require('dotenv').config();

server.connection({
  host: 'localhost',
  port: 2345
});

server.route(routes);

server.start(() => {
  console.log(chalk.green(`Server is running at ${server.info.uri}`));
});

ไฟล์ controller.js

'use strict';

const logger = require('./logger');

module.exports = {
  index: {
    handler: (request, reply) => {
      reply({message: 'OK'});
    }
  },

  ahoy: {
    handler: (request, reply) => {
      let payload = request.payload;
      logger(payload, 'payload');

      reply('Ahoy! How are you?');
    }
  }
}

ไฟล์ routes.js

'use strict';

const controller = require('./controller');

module.exports = [
  {
    method: 'GET',
    path: '/',
    config: controller.index
  },
  {
    method: 'POST',
    path: '/ahoy',
    config: controller.ahoy
  }
]

ไฟล์ logger.js

'use strict';

const chalk = require('chalk');

module.exports = (message, name) => {
  if (typeof message === 'object') {
    console.log(chalk.blue('============================'));
    console.log(chalk.blue(name));
    console.log(chalk.blue(JSON.stringify(message, undefined, 2)));
    console.log(chalk.blue('============================'));
  } else {
    console.log(chalk.blue('============================'));
    console.log(chalk.blue(name));
    console.log(chalk.blue(message));
    console.log(chalk.blue('============================'));
  }
}
โค๊ดส่วนนี้ผมการอัพขึ้น Hosting จริง มันจึงสามารถรับ trigger จาก slack ได้ หากเรารัน server บน localhost จะไม่สามารถรับ trigger ได้นะครับ :)

ทดลองพิมพ์ข้อความใน Slack ดูใหม่

Its work!

Step 3 : Implement Logic

ต่อมาเมื่อระบบโอเคแล้ว สามารถรับส่งข้อมูลระหว่าง Slack ได้แล้ว ต่อมาเราต้องมาทำตาม Guideline ของ Slack กันบ้าง ก็คือต้องทำการเช็ค validate ทุก request ฉะนั้นผมจึงทำการสร้างไฟล์ local variable แต่ขี้เกียจเซฟไว้ในเครื่องเลยใช้เป็น .env และใช้ https://www.npmjs.com/package/dotenv มาช่วย

สร้างไฟล์ .env ขึ้นมา ใส่ Slack Token ลงไป

SLACK_TOKEN=YOUR_SLACK_TOKEN_ID

จากนั้นในส่วน controller.js ผมก็จะเช็ค payload ที่ทาง slack ส่งมาก่อน เนื่องจากว่าส่งมาเป็น POST

payload ทั้งหมดที่ Slack ส่งมาคือ

{
  "token": "xxxxx",
  "team_id": "xxxxx",
  "team_domain": "devahoy",
  "channel_id": "xxxxx",
  "channel_name": "xxxxx",
  "user_id": "xxxxx",
  "user_name": "xxxxx",
  "command": "/ahoy",
  "text": "ahoy",
  "response_url": "xxxxx"
}

สิ่งที่เราต้องการตอนนี้คือ

  1. token : เพื่อเอาไว้เช็คว่าเป็น request จาก Slack จริงไหม
  2. command : อันนี้เอาไว้ตรวจสอบว่าผู้ใช้พิมพ์คำสั่งอะไรมา แต่ว่าเนื่องจากตอนนี้เรามีแค่คำสั่งเดียว ส่วนนี้อาจจะยังไม่จำเป็น
  3. text : ข้อความที่ถูกส่งมาพร้อมกับ command

ในส่วน logic ผมก็เลยเขียนใหม่เป็น

handler: (request, reply) => {
  let payload = request.payload;
  logger(payload, 'payload');
  if (payload.token !== process.ENV.SLACK_TOKEN) {
    return reply('unauthorized')
  }

  reply(`Ahoy! ${payload.user_name} How are you?`);
}

เนื่องจาก Slack สามารถ Custom response ข้อความได้ว่าจะส่งเป็นแบบ plain text หรือว่าจะส่งแบบกำหนด format เช่น เห็นแบบ private เฉพาะคนพิมพ์ หรือเห็นทั้ง channel (default เป็น private) เช่น

{
  "response_type": "in_channel",
  "text": "Good morning!",
  "attachments": [
    {
      "text": "How are you today?"
    }
  ]
}

ซึ่งผมจะใช้ payload.text เป็นการกำหนดว่าให้ส่ง response กลับไปแบบไหน

if (payload.text === 'private') {
  return reply(`Ahoy! ${payload.user_name} How are you?`);
} else {
  return reply({
    response_type: 'in_channel',
    text: `Ahoy! ${payload.user_name}`,
    attachments: [
      { text: `How are you? ${payload.user_name}` }
    ]
  });
}

เมื่อลองพิมพ์บน Slack ก็จะได้แบบดังภาพ เป็นอันเรียบร้อย :)

Finish Slash

นอกเหนือจากนี้เรายังสามารถ Custom logic ต่างๆทางฝั่ง Server ของเรา อาจจะมีการคำนวณ +-*/ คร่าวๆ ดึงข้อมูลสภาพอากาศ เช็คผลบอล เช็คโปรแกรมการแข่งขันต่าง ๆ หรือเช็ครอบหนัง แล้วส่งกลับไป Slack ก็ทำได้ ก็ลองๆเอาไปประยุกต์กันดูครับ หรือจะ Advanced กว่านั้นทำเป็น Slack App ไปเลยก็ได้ครับ

สุดท้าย Source Code ที่ใช้ในบทความผมอัพไว้บน Github ครับ

Source Code

Reference

Chai Chai Phonbopit : Web Developer @Nimbl3 • ผู้ชายธรรมดาๆ ที่ชื่นชอบ Node.js, JavaScript และ Open Source มีงานอดิเรกเป็น Acoustic Guitar และ Football

บทความล่าสุด