Pathee engineering blog

世界をしなやかに変えるエンジニアたちのブログ

Slackのリアクションを用いてタスクの対応状態を管理・確認できるようにした

エンジニアの消火器です。

以前こんな記事をかきました。

pathee.hatenablog.com

しかしこれを作った後、Slackが見れない人に対し、エンジニアの対応状況をリアルタイムに知れるような仕組みが欲しいという声が上がってきました。

確かにエンジニアが状況をスムーズにキャッチできるようにはなったけど、Slackを見れないライターさんにエンジニアが対応中ですよ、という状況を伝える手段ではないので、そのニーズもありそうですね…。

ということでこれを拡張したお話はまた次回。

ということだったので、今回はSlackのリアクションを用いてタスクの対応状態を管理・確認できるようにした話をします。

作るものの概要は以下のような感じです。

取得するメッセージの内容などはかなりニーズによるため適宜解説を省略し重要箇所の解説のみに留めます。

Slackのポストを受け取ってアクションを起こす

まずはSlack側の設定から。 Slack側では Slack内にメッセージが入力されたことリアクションが追加されたことを検知するためのBotの設定をしないといけません。

Bot自体の作成方法は省略します。 やるべきことは権限の設定と、RequestURLの設定です。

権限に関しては以下の権限を付与してください。

  • channels:history
  • reactions:read

もしも、プライベートチャンネルやDMをターゲットにしいたい場合は、それぞれ、channels:historyの代わりに im:historyim:historyを付与してください。

リクエストURLに関してはGAS側のURLを設定します。 こちらはGASの作成が完了しないとURLが発行されないので、その後に入力をお願いいたします。

GAS側でメッセージとリアクションを取得する

まずは章名の通りメッセージとリアクションを取得するコードを書きます。 Botからのレスポンスを受け取りその中を確認することで、Slack上で行われた行為をチェックいたします。

実際のコードは以下です ポイントは今回のレスポンスの内容が動作対象であるかどうかを確認する switch_action メソッドです。 slack_paramsの内容は、メッセージが追加されたときは

{
    "token": "one-long-verification-token",
    "team_id": "T061EG9R6",
    "api_app_id": "A0PNCHHK2",
    "event": {
        "type": "message",
        "channel": "C024BE91L",
        "user": "U2147483697",
        "text": "Live long and prospect.",
        "ts": "1355517523.000005",
        "event_ts": "1355517523.000005",
        "channel_type": "channel"
    },
    "type": "event_callback",
    "authed_teams": [
        "T061EG9R6"
    ],
    "event_id": "Ev0PV52K21",
    "event_time": 1355517523
}

リアクションが追加された時は、

{
    "type": "reaction_added",
    "user": "U024BE7LH",
    "reaction": "thumbsup",
    "item_user": "U0G9QF9C6",
    "item": {
        "type": "message",
        "channel": "C0G9QF9GZ",
        "ts": "1360782400.498405"
    },
    "event_ts": "1360782804.083113"
}

となっています。

api.slack.com

api.slack.com

他にも「グループにメッセージが投稿された」、「リアクションがついた」、「誰かがチャンネルに入室した」など様々なパターンがあります。

今回は上の2ケースを対象に処理を行いたいので、それらを判別するのに十分な材料である条件を指定しています チャンネルへのメッセージ追加は、type == "message"かつthread_ts == undefined、 リアクションの追加はtype == "reaction_addedで行っています。

今回、それ以外の動作は対象外なので、それ以外の場合は処理を終了させています。 チャンネルへのメッセージ追加が行われた場合は、メッセージの詳細を確認し、内容をスプレッドシートに転記するget_messageを、 リアクションの追加が行われた時は、リアクションの詳細を確認し、スプレッドシートのステータスを更新するget_statusを行います。

function doPost(e){
  var slack_params = JSON.parse(e.postData.getDataAsString()).event;
  // paramsの内容を確認し、メッセージの追加なのか、リアクションの追加なのかを分岐する
  action_type = switch_action(slack_params);
  switch (action_type) {
    case "MESSAGE_CHANNELS":
      // メッセージの詳細を確認し、内容をスプレッドシートに転記する
      get_message(slack_params);
      break;
    case "REACTION_ADDED":
      // リアクションの詳細を確認し、スプレッドシートのステータスを更新する
      get_status(slack_params);
      break;
    default:
      // 対応する必要のないアクションなので処理を終了
  }
}

function switch_action(slack_params){
  /*paramsの形式を確認し以降の処理分岐のためのフラグを返す。
  フラグ名はSlackのWorkspace Eventsに準拠。
  actionによってparams大きく構造が異なるため、ここで判定をする
  現在対応しているのは以下。
    ・MESSAGE_CHANNELS … チャンネルにメッセージが追加された
    ・REACTION_ADDED … リアクションが追加された
    ・上記以外のケースは null を返却する
  */
  if(slack_params.type === "reaction_added"){
    console.log("REACTION_ADDED")
    return "REACTION_ADDED";
  }
  else if(slack_params.type === "message" && slack_params.thread_ts === undefined){
    console.log("MESSAGE_CHANNELS")
    return "MESSAGE_CHANNELS";
  }
  else {
    console.log("OTHER")
    return null;
  }
}

特定のメッセージを取得し、スプレッドシートに転記する

メッセージの詳細を確認し、内容をスプレッドシートに転記するget_messageの実装です。

メッセージの時の中の形式は以下のようになっているので、 その中からメッセージ内容slack_textの他に、 投稿されたチャンネル情報channel、 メッセージの投稿日時tsを取得します。

{
"type": "message",
"channel": "C024BE91L",
"user": "U2147483697",
"text": "Live long and prospect.",
"ts": "1355517523.000005",
"event_ts": "1355517523.000005",
"channel_type": "channel"
}

投稿されたメッセージの内容が抽出したいメッセージであるかを判別するために、slack_textchannelを使います。 channelは不要かもしれませんが、抽出したい内容がもし別チャンネルにポストされた場合、その内容もスプレッドシートに加えてます(もっといい方法ないかな…)

tsは、これを利用することでSlackのメッセージを示すURLが作れるという目的と、 これをスプレッドシートに保存しておくことで、リアクションが対象のメッセージについてステータスを変更する処理を行えるようにしてます。

抽出の部分は、抽出したい文面によって大きく変わるので参考程度に 自分たちの場合はSentryのUserFeedbackメッセージである以下の形式の文章を抽出していました。

SentryにUserFeedbackが追加されました
Reporter  : `[報告者]`
Issue         : `[イシュー内容]`
Feedback : `[エラー内容]`
Link           : `[URL]`

なので、テキストを行ごとに分解、 1行目の文言を含んでいる場合、他の要素を適切な形に分解し内容だけを抽出する処理を書いています。 startsWithRegTextがそれぞれ、文章が決まった文字で始まっているかを判定するメソッド、必要な部分だけを正規表現で抽出するメソッドになっています。

最後にsheet.appendRowを使いスプレッドシートに書き込みを行います。 これで文章の転記GASは完成です。

var CHANNEL_ID = "メッセージを監視したいチャンネルID"
var SPREADSHEET_ID = "スプレッドシートのID"
var SHEET_NAME = "書き込むシート名"

function get_message(slack_params){
  slack_channel_id = slack_params.channel
  slack_text = slack_params.text
  slack_message_ts = slack_params.ts
  // #対象のチャンネルでなければ return null
  if (slack_channel_id != CHANNEL_ID){
    return null;
  }

  // 記述対象のスプレッドシートを指定
  var spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
  // 記述対象のシートを指定
  var sheet = spreadsheet.getSheetByName(SHEET_NAME);
  
  // シート中で文字列の入っている最終行を取得
  var last_row_num = sheet.getLastRow();

  // メッセージの中から、スプレッドシートに記入する項目を抽出
  var today = Utilities.formatDate( new Date(), 'Asia/Tokyo', 'yyyy/MM/dd');  
  var issue = ''
  var link = ''
  var feedback = ''
  var Reporter = ''
  var create_flag = 0
  arr = slack_text.split(/\r\n|\r|\n/);
  for (i = 0; i < arr.length; i++) {
    // 対象とするメッセージが文中に含まれていれば、シートへの転機の対象
    if(arr[i] === "SentryにUserFeedbackが追加されました"){
      create_flag  = 1
    }else if(startsWith(arr[i], "Issue")){
      issue = RegText(arr[i])      
    }else if(startsWith(arr[i], "Link")){
      link = RegText(arr[i])            
    }else if(startsWith(arr[i], "Feedback")){
      feedback = RegText(arr[i])      
    }else if(startsWith(arr[i], "Reporter")){
      Reporter = RegText(arr[i])      
    }
  }

  // 転機対象であれば、抽出した内容をシートに書き込む
  if(create_flag === 1) {
    sheet.appendRow([last_row_num, '未着手',today , Reporter, issue, feedback, link, "https://corp-pathee.slack.com/archives/"+slack_channel_id+"/p"+slack_message_ts.replace(".","") ]);
  }
}

function startsWith(string, searchString) {
  return (string.indexOf(searchString) === 0); 
}

function RegText(string){
  var RegExp = /`<?([\s\S]*?)>?`/;
  return string.match(RegExp)[1]
}

特定のメッセージについたをリアクションを取得し、対応するスプレッドシートの内容を更新する

次は特定のメッセージについたをリアクションを取得し、対応するスプレッドシートの内容を更新するget_statusの実装です。

リアクションの時の中の形式は以下のようになっているので、 その中から絵文字内容reactionの他に、 投稿されたチャンネル情報item.channel、 絵文字のついたメッセージの投稿日時item.tsを取得します。 それぞれメッセージと形式が違うので注意してください。

{
    "type": "reaction_added",
    "user": "U024BE7LH",
    "reaction": "thumbsup",
    "item_user": "U0G9QF9C6",
    "item": {
        "type": "message",
        "channel": "C0G9QF9GZ",
        "ts": "1360782400.498405"
    },
    "event_ts": "1360782804.083113"
}

チャンネルの判定は前と同じ。 ポイントは、シートの中のタイムスタンプ記述列から、スタンプがついたメッセージのタイムスタンプを探す処理です。 そのために、シートの中の特定の列に入っている値をリストで取得したかったのですが、これがちょっと難しく………………。

悩んでいたところにクリティカルな内容があったのでそれをお借りしました。 この場を借りてお礼を申し上げます、ありがとうございます。

qiita.com

上の記事にあるメソッド convertTwoDimensionToOneDimension を利用し、シートの中から特定の列をchannel_ts_colmunに取得、 indexOfで絵文字のタイムスタンプが含まれているかを調べます。

含んでいたら、その行のステータスを絵文字に合わせて更新します。

これで、特定のメッセージについた特定の絵文字に従い、セルの内容を更新する仕組みができました。

function get_status(slack_params){
  slack_channel_id = slack_params.item.channel
  slack_message_ts = slack_params.item.ts
  slack_emoji = slack_params.reaction
  // 対象のチャンネルでなければ return null
  if (slack_channel_id != CHANNEL_ID){
    return null;
  }
  
  // 記述対象のスプレッドシートを指定
  var spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
  // 記述対象のシートを指定
  var sheet = spreadsheet.getSheetByName(SHEET_NAME);

  // sheetのデータを取得して、チャンネルIDとタイムスタンプを照合する
  var sheetData = sheet.getDataRange().getValues();
  var channel_ts_colmun = convertTwoDimensionToOneDimension(sheetData, 7) // チャンネルIDとタイムスタンプ列 : 7
  var matched_row_num = channel_ts_colmun.indexOf("https://corp-pathee.slack.com/archives/"+slack_channel_id+"/p"+slack_message_ts.replace(".","")) + 1 // 0は該当なし
  
  // sheetのデータを取得して、チャンネルIDとタイムスタンプを照合する
  if (matched_row_num == 0){
    return null;
  }

  # セルに記入されたステータスを絵文字に合わせて更新する
  var current_status = sheet.getRange(matched_row_num, 2).getValue()
  console.log(current_status)
  if(current_status == "完了"){
    return null;
  }else if(current_status != "完了" && slack_emoji=="done"){
    sheet.getRange(matched_row_num, 2).setValue("完了")
  }else if(current_status != "保留中" && slack_emoji=="horyuu"){
    sheet.getRange(matched_row_num, 2).setValue("保留中")
  }else if(current_status != "対応中" && slack_emoji=="taioutyuu"){
    sheet.getRange(matched_row_num, 2).setValue("対応中")
  }else if(current_status != "確認中" && slack_emoji=="kakuninsimasu"){
    sheet.getRange(matched_row_num, 2).setValue("確認中")
  }
}

完成

上記GASのリクエストURLをSlackにセットして完成です。 スクショだけでは分かりませんが、 特定のメッセージがポストされると自動でシートの行が追加され、 特定のリアクションがついたら自動でステータスを更新されます。

f:id:pathee:20191221223903p:plain

これでエンジニアの対応状況の見える化ができました。

SlackとGASを用いた仕組みは手軽にいろいろなことができるので、今後もちょっとずつやっていきたいと思います。

ありがとうございました。