ナレッジを収集するslack botの作成~GASを使って~

ナレッジを収集したい

slack上のナレッジを収集するslack botを作成しよう。アイデアの着想を得たのは、ある企業の人をお話から。その企業はslack上の会話によって蓄積されるナレッジを体系的な収集するslack botを作成したらしい。

私も個人開発でslackを使っている。単純にチャンネルごとにメモを分けるだけでも、タスク管理の効果があるし、スター機能を使えば重要な情報を管理できる。しかし、スター付けるほどではない情報やメソッド(これらをナレッジと呼ぼう)も多数あり、これらを効率的に管理する枠組みが必要であると感じていた。よし、私もナレッジを管理できるslack botを作成しよう。

どうやら、その企業のbotは言語処理モデルも組み込んで非常に高度なことをやっているようだが、今回は簡単に明示的にコマンドを打つことによってナレッジを保存するシステムを作成しようと考えている。

機能

  • ナレッジのジャンルを登録できる機能
  • ジャンルごとにナレッジを登録できる機能
  • spread sheetに追加したナレッジをジャンルごとにslack上で呼び出せる機能

slack botの作り方

slack botの作成には、slack側の設定と実際アプリが機能する環境を用意する必要がある。ここではslack側の設定をまとめる。 タスクフローは以下の通り。

  1. slack apiにアクセス(https://api.slack.com/apps)
  2. Create New Appからアプリを作成
  3. OAuth設定
    1. 左側メニューからFeatures→OAuth&Permissions
    2. 「app_mentions:read、chat:write」の2つのスコープを与える

 

f:id:kpsdr:20200302213327p:plain
アプリのスコープ

  • Event Subscriptionsの設定
    • slack上のイベントをアプリに通知するための設定です
    • webアプリのurlが必要なので後で設定します

GAS(Google App Script)が便利

実際にアプリが動作する環境としてGAS(Google App Script)を利用しよう。googleアカウントさえあれば、すぐGASの実行環境を作成できる。また、ボタン1つでwebアプリとして公開することも可能だ。

GASプロジェクトの立ち上げ、SlackAppライブラリの導入

私が参考にしたサイトを載せる。(分かりやすい) qiita.com

Bot User OAuth Access Tokenの登録

先ほどのslack apiのアプリ設定画面を開きます。 Features→OAuth&PermissionsにあるBot User OAuth Access Tokenをコピーしてください。

そして、そのコードをGASのプロジェクトに登録します。

  1. ファイル→プロジェクトのプロパティ→スクリプトのプロパティ
  2. プロパティ: SLACK_ACCESS_TOKEN, 値: 先ほどコピーしたコード

コードピックアップ

コードの中でポイントとなるところだけをpick upします。

const params = JSON.parse(e.postData.getDataAsString());
=========================================================
return ContentService.createTextOutput(params.challenge);

params.challengeをreturnしてあげることで認証が完了します。

function writeLog(text){
  let spreadSheet1 = SpreadsheetApp.openByUrl('ログ用spread sheetの共有url');
  let sheet1 = spreadSheet1.getSheets()[0];
  // 値の書き込み
  sheet1.appendRow([new Date(), text]);
}

デバックするための関数。 GASはGCPとの連携をしないとデバックが困難。 適当なspread sheetを用意して、そのシートにログを出力しながらデバックするのがよし。

参考にさせてもらいました。

Google Apps Scriptでログをスプレッドシートに書き出す方法

コード

let spreadSheete;
let sheet;

function doPost(e) {
  try{
  const token = PropertiesService.getScriptProperties().getProperty('KNWL_SLACK_ACCESS_TOKEN');  
  const slackApp = SlackApp.create(token);

  const params = JSON.parse(e.postData.getDataAsString());
  const commands = params.event.text.split(' ');
  // 返送メッセージ
  let message;
  
  /* コマンド解析 */
  const action = commands[1];
  const genre = commands[2];
  let content;
  if(commands.length == 3){
    content = null;
  }else if(commands.length == 4){
    content = commands[3];
  }else{
   const options = {
    channelId: "#general",
    userName: "knwl",
    message: "書式に違反しています"
   }
   slackApp.postMessage(options.channelId, options.message, {username: options.userName});
   // validation
   return ContentService.createTextOutput(params.challenge);
  }
      
  /* ファイルを開く */
  spreadSheet = SpreadsheetApp.openByUrl('ナレッジを書き込むspread sheetの共有url');
  sheet = spreadSheet.getSheets()[0];
  
  /* アクションごとに関数を呼び出す */
  switch(action){
    case 'setGenre':
      message = setGenre(genre); // ジャンルをセットする
      break;
    case 'set':
      message = setInfo(genre, content); // あるジャンルにナレッジをセットする
      break;
    case 'get':
      if(genre == "genres"){ 
        message = getGenres(); // ジャンル一覧を取得する
      }else{
        message = getGenreInfo(genre); // あるジャンルのナレッジをすべて取得する
      }
      break;
    default:
      break;
  }
  
  /* メッセージの送信 */
  let options = {
    channelId: "#general",
    userName: "knwl",
    message: message
  }
  // send message
  slackApp.postMessage(options.channelId, options.message, {username: options.userName});
  
  /* validation */
  return ContentService.createTextOutput(params.challenge);
  }catch(err){
    writeLog(err);
  }

}

function doGet(e){
  doPost(e);
}

function setGenre(newGenre){
  // 最終列を取得
  const lastCol = sheet.getLastColumn();
  // ジャンルが1つも存在しないとき
  if(lastCol == 0){
    sheet.getRange(1, 1).setValue(newGenre);
    sheet.getRange(2, 1).setValue(2);
    return '正常にジャンルを登録しました。 '
  };
  
  // すべてのジャンル名を取得
  const genres = sheet.getRange(1,1,1,lastCol).getValues()[0];
  
  if(genres.indexOf(newGenre) == -1){
    sheet.getRange(1, lastCol+1).setValue(newGenre);
    sheet.getRange(2, lastCol+1).setValue(2);
    return "正常にジャンルを登録しました。"
  }else{
    return "そのジャンルはすでに登録されています。"
  }
}

function setInfo(genre, info){
  // 最終列を取得
  const lastCol = sheet.getLastColumn();
  if(lastCol == 0){ return 'ジャンルが1つも存在しません '};
  
  // すべてのジャンル名を取得
  const genres = sheet.getRange(1,1,1,lastCol).getValues()[0];
  
  if(genres.indexOf(genre) == -1){
    return "ジャンルが存在しません"
  }else{
    const genreCol = genres.indexOf(genre) + 1;
    const genreRow = parseInt(sheet.getRange(2,genreCol).getValues()[0]);
    // infoの書き込み
    sheet.getRange(genreRow+1, genreCol).setValue(info);
    // genreの最終行の記録
    sheet.getRange(2,genreCol).setValue(genreRow+1);
    return "正常にナレッジを登録しました。"
  }
}

function getGenres(){
  // 最終列を取得
  const lastCol = sheet.getLastColumn();
  if(lastCol == 0){ return 'ジャンルが1つも存在しません '};
  
  // すべてのジャンル名を取得
  const genres = sheet.getRange(1,1,1,lastCol).getValues()[0];
  message = genres.join(' ');
  return message;
}

function getGenreInfo(genre){
   // 最終列を取得
  const lastCol = sheet.getLastColumn();
  if(lastCol == 0){ return 'ジャンルが1つも存在しません '};
  
  // すべてのジャンル名を取得
  const genres = sheet.getRange(1,1,1,lastCol).getValues()[0];
  
  if(genres.indexOf(genre) == -1){
    return "ジャンルが存在しません"
  }else{
    const genreCol = genres.indexOf(genre) + 1;
    const genreRow = parseInt(sheet.getRange(2,genreCol).getValues()[0]);
    // genreのinfoを取得
    const allInfo = sheet.getRange(3, genreCol, genreRow - 2, 1).getValues();
    return allInfo.join(' ');
  }
}

function writeLog(text){
  let spreadSheet1 = SpreadsheetApp.openByUrl('ログ用spread sheetの共有url');
  let sheet1 = spreadSheet1.getSheets()[0];
  
  // 値の書き込み
  sheet1.appendRow([new Date(), text]);
}

Events Subscriptionsの設定

さっき保留していたEvents Subscriptionsの設定。webアプリケーションのurlをslack apiの設定画面に登録する。

  • webアプリケーションの公開
    • GAS上のメニュー→webアプケーションとして導入から更新
    • urlをコピー

    ※ プログラムを編集する度にProject VersionをNewにして更新

    ※ 公開範囲を「Anyone, even anonymous」に

f:id:kpsdr:20200307173127p:plain
webアプケーションとして公開

  • Events Subscriptionsの設定
    1. slack apiのアプリ設定画面からEvents Subscriptionsへ
    2. Enable EventsをOnに
    3. 先ほどコピーurlをRequest URLに張り付け
    4. slack上で検知するeventを設定 「Subscribe to bot events」で 「app_mention 」を設定

f:id:kpsdr:20200307174112p:plain
Event Subscriptionsの設定

完成!! 各コマンドの機能を説明

  • @アプリ名 setGenre ジャンル1
    • ジャンル1をspread sheetにジャンルとして登録する
  • @アプリ名 set ジャンル1 ナレッジ1
    • ナレッジ1をジャンル1に登録
  • @アプリ名 get genres
    • 登録されている全ジャンルを表示
  • @アプリ名 get ジャンル1
    • ジャンル1のすべてのナレッジを取得し、slack上に表示する

まとめ

はじめてslack botを作ったが、GASが便利すぎる。自分でサーバーも用意しなくていいし、deployして外に公開するのもボタン1つ。この簡易性は代えがたい。ただ、GAS単体ではデバックがしにくかったり、オンラインの開発環境なのでgit等での管理ができなかったり(やり方はあるかも・・・)不便なまだまだは多い。それでも、簡単なslack botを作るぐらいなら非常に有用性があることは間違いなさそう。

参考サイト