Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

bot用フレームワークなしでslack botを作る

More than 1 year has passed since last update.

はじめに

以前slack botを作ろうとbotkitを用いて作成していたのですが、herokuで稼働した時にちょっとした問題があったのでbotkitなしでslack botを作った際の知見を共有したいと思います。
*記事書くのは初めてなので拙いと思います。

botkitとherokuの問題点と解決策

問題は2つ
1. botkit内でexpressを使用しているため、1つのプロセス内に他の機能を盛り込めないこと
2. herokuの無料枠では1つのプロジェクトにつき1つのプロセスしか動作できないこと

この2つがいい感じに噛み合ってしまい、slack botの機能と外部に提供するAPI機能の両立ができないのです。
この問題の解決策として、機能を追加するための利便性とbot稼働とAPIサーバを両立するためにはbotフレームワークなしで自作するという考えに至りました。
自分でexpressを立てればbot以外の機能を盛り込みつつ、1つのプロセスで動かすことができるからです。

環境

Node.js (v11.11.0)
ESMAScript2015

Babelは導入するのが面倒臭かったので使わず、拡張子をmjsにして動作させています。
参考にした記事: Node.js(v8.5.0以降)でBabelやwebpackを利用せずにimport/exportを利用する

まずは公式ページでbotを作成

https://api.slack.com/
ここからStart Building → Create New Appでslack botを作成します。
start_building.png

今回のbot名は偶々目に入った可愛いeevee(イーブイ)にしました。
ワークスペースは共用の場だと色々と迷惑になるので自分専用のワークスペースです。

このままではワークスペースに登録するbotがないのでBotUSerページのAdd a Bot Userで追加します。

Oauthを通す

https://api.slack.com/authentication/oauth-v2 のページ中程にあるフロー画像の通りに進めていきます。
そのために先ずはRedirect URLを設定します。
oauth_permission.png
Oauth&PermissionsページのAdd New Redirect URLにhttp://localhost:8000/oauthと設定します。
また後々Real Time Messaging APIを使ってWebSocketを通すので、その時用にScopesにAdmin, identify ,botの3つを追加しておきます。

以下oauth認証まで行うコードです。
access_tokenが取れればOKです。
CLIENT_IDやCLIENT_SECRETは.evnファイルに記述し、dotenvを使うことで環境変数としています。

// 環境変数用
import dotenv from 'dotenv';
dotenv.config();

import request from "request";
import express from "express";
import bodyParser from 'body-parser';

const app = express();

// port番号設定
app.set('port', (process.env.PORT || 8000));

// サーバ起動時
app.listen(app.get('port'), function () {
  console.log('server launched');
});

// bot oauth認証用
app.get('/oauth', function (req, res) {
  res.header('Content-Type', 'text/plain;charset=utf-8');

  let code = req.query.code;
  if(code){
    res.send('oauth setting is collect');
    oauth(code);
  }
  else{
    res.send('oauth setting is wrong');
  }
});

function oauth(code) {
  request.post({
    url: 'https://slack.com/api/oauth.v2.access',
    form: {
      code: code,
      client_id: process.env.CLIENT_ID,
      client_secret: process.env.CLIENT_SECRET
    }
  }, function (err, res, body) {
    let access_token = JSON.parse(body).access_token;
    console.log(access_token);
  });
}

コードの実行はterminalにnode --experimental-modules main.mjsで動きます。
これでダメな場合は node --experimental-modules --es-module-specifier-resolution=node main.mjsとすれば動くことがあります。
実行するためにだらだらと書く必要がありますが、Babelの設定が要らないので環境導入でつまずく心配がないです。

次にoauthフロー画像の下にあるURLをブラウザに打ち込みます。
https://slack.com/oauth/v2/authorize?scope=incoming-webhook,commands&client_id=xxxxxxx.xxxxxxxxxxx
xxxxxxx.xxxxxxxxxxxは自身のCLIENT_IDに書き換えてください。
oauth_redirect.png
URLをブラウザに打ち込んでこのような画面が出てきたらOKです。
適当にチャンネルを選択してAllowを押し、エラーがなければterminalにaccess_tokenが出るのでメモっておきましょう。
もしメモり忘れてもOauth&PermissionsページのOAuth Tokens & Redirect URLsに記述されています。xoxbの方です。

RTMを用いてWebSocketを通す

RTMの詳しいことはこちらに書いてあります。https://api.slack.com/rtm
メモっておいたaccess_tokenも.envファイルに書き込んでおきます。

// サーバ起動時
app.listen(app.get('port'), function () {
  console.log('server launched');
  // 新しく追加
  rtmStart();
});

// Real Time Messageing APIの開始
function rtmStart() {
  request.get({
    url:'https://slack.com/api/rtm.start',
    qs: {
      token: process.env.ACCESS_TOKEN
    }
  }, function (err, res, body) {
    let websocketURL = JSON.parse(body).url;
    socket(websocketURL);
  });
}

import WebSocket from 'ws';
let ws = null;

function socket(url) {
  ws = new WebSocket(url);

  // websocketが繋がった時
  ws.on('open', function () {
    console.log('Open WebSocket');
  });

  // botが居るチャンネルで何か書き込まれた時
  ws.on('message', function (data) {
    data = JSON.parse(data);
    // slackに書き込んだ言葉を表示する
    console.log(data.text);
  });

  // websocketが閉じた時
  ws.on('close', function (data) {
    console.log('Close WebSocket');
  });
}

これでWebSocketが通りました。
botが居るチャンネルで何か打つとその言葉がterminalに表示されるます。
また、最初に必ずdata.typeがhelloだけを持つdataが通るのでundefindも出ているはずです。

slack botに機能追加

これでようやく準備が終わり、思うがままにslack botに機能を追加できます。
ws.on('message', function (data)内のconsole.log(data.text);の下にmainProccess関数を呼んであげるように追記します。

// ここがslackからデータを受け取るメイン処理
function mainProcess(data) {
  // 接続時にhelloが通るので
  if (data.type === 'hello')
    return;

  // ハウリングするのでbotには反応しない
  if (data.bot_id !== undefined)
    return;

  let type = data.type;
  if (type === 'message') {
    // subtypeを持っているmessage typeには反応しない
    if (data.subtype !== undefined)
      return;

    let text = data.text;
    let is_mention = text.match(/<@(.*)/);
    let mention_user = undefined;
    let mention_text = undefined;
    // メッセージではなくファイルならfile_idが存在する
    let file_id = data.files !== undefined ? data.files[0].id : undefined;

    // メンションが付いているなら文章本体だけを抜き出す
    if (is_mention) {
      let tmp = text.split('<@')[1];
      mention_user = tmp.split('> ')[0];
      mention_text = text.split('> ')[1];
    }

    // Message型はuser_id, text, channel_id, ts, is_mention, mention_user, mention_text, file_idのプロパティを持つ
    let message = new Message(data.user, text, data.channel, data.ts, is_mention, mention_user, mention_text, file_id);
    switchProcess(message);
  }
}

// メンション有り無しやコマンドに応じた分岐処理
function switchProcess(message) {
  // botにメンションなら
  if (message.mention_user === process.env.BOT_ID) {
    // 記事を表示
    if (message.mention_text === 'help')
      api.deleteMessage(message.channel_id, message.ts);
      api.postEphemeral(message.channel_id, 'Qiita記事のページだよ\nhttps://qiita.com/KessaPassa/items/a82b972b7fdedd2e13f7', message.user_id);
  }
}

// メッセージ削除
function deleteMessage(channel, ts, time = 30 * 1000) {
  setTimeout(function () {
    request.post({
      url: 'https://slack.com/api/chat.delete',
      form: {
        token: process.env.ACCESS_TOKEN,
        channel: channel,
        ts: ts,
        as_user: true
      }
    }, function (err, res, body) {
      if (err) throw err;
    });
  }, time);
}

// 普通にメッセージを送る
function postMessage(channel, text) {
  request.post({
    url: 'https://slack.com/api/chat.postMessage',
    form: {
      token: process.env.ACCESS_TOKEN,
      channel: channel,
      text: text
    }
  }, function (err, res, body) {
    if (err) throw err;
  });
}

@eevee helpとbotにメンションをつけてhelpとするとこのQiita記事のURLが返事として返ってきます。

因みにMessage型の中身はこちらになります。

const _user_id_message = Symbol();
const _text_message = Symbol();
const _channel_id_message = Symbol();
const _ts_message = Symbol();
const _is_mention_message = Symbol();
const _mentionUser_message = Symbol();
const _mentionText_message = Symbol();
const _file_id = Symbol();

export class Message {
  constructor(user_id, text, channel_id, ts, is_mention, mention_user, mention_text, file_id) {
    this[_user_id_message] = user_id;
    this[_text_message] = text;
    this[_channel_id_message] = channel_id;
    this[_ts_message] = ts;
    this[_is_mention_message] = is_mention;
    this[_mentionUser_message] = mention_user;
    this[_mentionText_message] = mention_text;
    this[_file_id] = file_id;
  }

  get user_id() {
    return this[_user_id_message];
  }

  get text() {
    return this[_text_message];
  }

  get channel_id() {
    return this[_channel_id_message];
  }

  get ts() {
    return this[_ts_message];
  }

  get is_mention() {
    return this[_is_mention_message];
  }

  get mention_user() {
    return this[_mentionUser_message];
  }

  get mention_text() {
    return this[_mentionText_message];
  }

  get file_id() {
    return this[_file_id];
  }
}

最後に

oauth認証してWebSocktを通せば後はただのプログラミングなのでslack botに色々な機能を持たせることができます。
是非switchProccess関数に色んな処理を追加して自分好みのslackにカスタマイズしてみてください!

この記事を書くに当たって、検証しつつ部分部分を取り除いた形になるのでわかりにくいところがあるかと思います。
ちょっとoauth関係が古いですが現在も弊研究室で稼働しているbotの中身がgithubで公開しているのでそちらを参考にしてみてください。
分報やチャンネル毎にメモ機能など便利なコマンドを追加しています。
https://github.com/KessaPassa/slackbot-extensions

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away