10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Slackで営業日まで待ってメッセージを送信できるSlashコマンドを作った

Last updated at Posted at 2019-09-16

前職の先輩である@shoitoさんが似たようなことをやった記事を公開していたので便乗してみました。
作った動機は似たようなもんなので割愛。

ちなみに作ったのは僕の方が先なんだからね!

作ったもの

/sendinworktime {メッセージ}と叩くと次の営業日のAM9:00まで待ってからメッセージを送信してくれるSlashコマンド。
/sendinworktime {返信したいメッセージのリンク} {メッセージ}とするとスレッド内にも送信できます。

メッセージはBotから送信され、誰が送信したのかわかるようにメッセージの最後にfrom @{送信者のusername}が付きます。

説明 画像
送信 image.png
Botからの返信 image.png
実際に送られるメッセージ image.png

Slack App+GASで動かしているので、以下にどう作ったかについて書いていきます。
やった作業としてはSlash CommandsとGASでSlackのオリジナルコマンドをつくるとほぼ同じです。
わからなくなったら参照してください!(丸投げ)

Slack側での作業

Slack Appの作成

  1. https://api.slack.com/apps の「Create New App」から新規Appを作成します。
  2. App Nameとインストール先のSlackワークスペースを選択します。僕はApp Nameを「Send In Work Time」としました。
  3. Basic InformationのBots、Permissionsを選択します。
    1. BotsではDisplay Name,Default usernameともに「send_in_worktime」としました。
    2. Botsでの作業をするとPermissionsには自動でチェックが付きます。Install App to Workspaceをするとメッセージ送信するためのTOKENが表示されるのでBot User OAuth Access Tokenをコピーしておきます。

一旦Slack側の作業としてはこれで終了です。
GASの準備を終えてからSlash Commandsの設定をする必要がありますが、それは後述。

GAS側での作業

https://script.google.com にアクセスしてコードを書いていきます。

var TARGET_HOUR = '9';
var TARGET_MINUTES = '00';

// main
function doPost(e) {
  var props = {
    "username": e.parameter.user_name,
    "team_domain": e.parameter.team_domain,
    "channel_id": e.parameter.channel_id,
    "text": e.parameter.text,
  };
  
  // schedule message
  var result = scheduleMessage(props);
  
  // response for command
  var text;
  if(result.ok) {
    text = "Success: message scheduled at " + Utilities.formatDate(new Date(result.post_at * 1000), "JST", "YYYY'/'MM'/'dd'('EE) ") + TARGET_HOUR + ":" + TARGET_MINUTES + '\n```\n' + result.message.text + "\n```";
  } else {
    text = "Failure: because of error `" + result.error + "`";
  }
  var response = { text: text };
  return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
};

function scheduleMessage(props) {
  var users = getUserList();
  var splited = props.text.split(" ");
  if(splited.length <= 1) {
    return send(props, users);
  }
  
  var regex = new RegExp("https:\/\/" + props.team_domain + "\.slack\.com\/archives\/([^/]*)\/p([^\?]*).*(thread_ts=([\\d.]+))?");
  var match = splited[0].match(regex);
  if(match) {
    return send({
      username: props.username,
      team_domain: props.team_domain,
      channel_id: match[1],
      ts: match[4] || match[2].substr(0, match[2].length - 6) + "." + match[2].substr(match[2].length - 6),
      text: splited.slice(1).join(" "),
      original: props.text,
    }, users);
  } else {
    return send(props, users);
  }
}

function send(data, users) {
  var user = getUser(data.username, users);
  data.text = convertMessage(data.text, users) + "\n\nfrom: `@" + (user ? user.display_name : data.username) + "`";

  var options = {
    text: convertMessage(data.text, users),
    channel: data.channel_id,
    post_at: (getTargetDate(new Date()) / 1000).toFixed(), 
  };
  if(data.ts) {
    options.thread_ts = data.ts;
  }

  return callSlackAPI("chat.scheduleMessage", options, "post");
}

function convertMessage(text, users) {
  var temp = text;
  users.forEach(function (user) {
    temp = temp.replace(new RegExp("@" + user.display_name + "(\\s|$)", 'gm'), "<@" + user.id + "> ");
    temp = temp.replace(new RegExp("@" + user.name + "(\\s|$)", 'gm'), "<@" + user.id + "> ");
  });
  return temp;
}

function getUserList() {
  var cache = CacheService.getScriptCache();
  var data = cache.get("users");
  if(data) {
    return JSON.parse(data);
  }
  
  var res = callSlackAPI("users.list", {});
  var users = res.members.filter(function(member) {
    return !member.is_bot;
  }).map(function(member) {
    return {
      id: member.id,
      name: member.name,
      display_name: member.profile.display_name || member.name,
      icon: member.profile.image_72
    };
  });
  cache.put("users", JSON.stringify(users), 60*60*24 - 10 * 60); 
  return users;
}

function getUser(username, users) {
  var filtered = users.filter(function (user) {
    return user.name === username;
  });
  return filtered.length === 1 ? filtered[0] : null;
}

function getTargetDate(base) {
  var temp = new Date(base.getTime());
  temp.setHours(Number(TARGET_HOUR));
  temp.setMinutes(Number(TARGET_MINUTES));
  temp.setSeconds(0);
  
  if(base.getTime() < temp.getTime() && !isHoliday(base)) {
    return temp;
  }

  do {
    temp.setDate(temp.getDate() + 1);
  } while(isHoliday(temp))
  return temp;
}

function isHoliday(date){
  var weekInt = date.getDay();
  if(weekInt <= 0 || 6 <= weekInt){
    return true;
  }

  var calendarId = "ja.japanese#holiday@group.v.calendar.google.com";
  var calendar = CalendarApp.getCalendarById(calendarId);
  var events = calendar.getEventsForDay(date);
  if(events.length > 0){
    return true;
  }

  return false;
}

function callSlackAPI(api, payload, method){
  payload.token = PropertiesService.getScriptProperties().getProperties().SLACK_TOKEN;

  var options = {
    "method" : method || "get",
    "payload" : payload
  };
  var res = JSON.parse(UrlFetchApp.fetch("https://slack.com/api/" + api, options));
  return res;
}

メニューの公開 > ウェブ アプリケーションとして導入...で公開しておきます。
アプリケーションにアクセスできるユーザーを「全員(匿名を含む)」にするのを忘れずに!
導入を押した後に表示されるURLはSlackとの連携に使うのでコピーしておいてください。

コードが汚いのは許してください :bow:
気が向いたら直します!(直すとは言ってない。一回動かしてしまうと直すのめんどくさいんだよなぁ…)

doPost

Slashコマンドが叩かれた時に呼ばれる関数です。

  1. Slackから渡されたイベントから必要な情報を取り出す
  2. メッセージ登録用の関数を呼び出す
  3. コマンドを叩いたユーザへの返信

を行っています。

scheduleMessage

スレッドへの返信のためにURLがメッセージの最初についているかどうかを見てSlackのAPIにわたす情報を書き換えています。
やってることと関数名があってないのはすいません… :bow:

send

SlackのAPIにわたす情報をさらに書き換えています。
例によって関数名がひどい。

  1. textの最後にfrom @{送信者のusername}を足す
  2. textを@{username}<@{userId}>に変換したものに差し替え
  3. post_atを次の営業日のAM9:00に
  4. (返信するなら)thread_tsを返信したいメッセージのものに

convertMessage

↑の@{username}<@{userId}>を実際に行っています。

getUserList

↑の変換を行うためにSlackのワークスペース内のユーザ一覧を取得しています。
実際にコマンドを叩いたら遅くてタイムアウトしてしまったのでキャッシュしています。(Slashコマンドの返信までの制限時間)

getUser

↑のユーザ一覧から特定のユーザを取り出します。

getTargetDate

次の営業日のAM9:00を計算します。

isHoliday

休日(日本)かどうかを判定します。

callSlackAPI

SlackのAPIを叩きます。
今回はメッセージ送信の予約ができるchat.scheduleMessageを使用しています。

PropertiesService.getScriptProperties().getProperties().SLACK_TOKEN;ファイル > プロジェクトのプロパティ > スクリプトのプロパティに登録した値を取り出しています。
Slack側での作業でコピーしておいたBot User OAuth Access Tokenを設定しています。

SlackとGASの連携

https://api.slack.com/apps にアクセスするとSlack側での作業で作成したAppが表示されているので選択します。

  1. Basic Informationを選択
  2. Add features and functionality内のSlash Commandsを選択し、以下のように設定
    1. Nameに「sendinworktime」
    2. Descriptionには「営業日まで待ってからメッセージを送信します」
    3. Request URLはGASでの公開時に表示されるURL

これで作業は終了です。
Slackから/sendinworktime {メッセージ}と送信すると次の営業日のAM9:00まで待ってメッセージを送ることができるようになっているはずです :tada:

今後について

やりたかったけどできてないことがいくつかあるのでそれをやっていきたいなと思っています。(やったらまた書くかも)

本人に偽装する

今回は送信されるメッセージはSend In Work Timeから送信されていますが、本当ならコマンドを叩いた本人からのように送信したいと思っていました。
できなかったのはchat.postMessageでは設定することが出来るusernameやicon_urlがchat.scheduleMessageでは使えなかったからです。

これを実装するためには実際に送信したい時間まで待ってからchat.postMessageを叩いてあげる必要があります。
GASで実装する場合には、doPostでSlackから受け取ったメッセージをスプレッドシートに一旦保存しておいて、日次のトリガーで保存されたメッセージをchat.postMessageで送信します。

ファイル送信に対応する

これは調べてないのでできるのかどうかよくわかってないです。
現状ではGoogle Driveなどに一度アップロードした後にそのURLをメッセージで共有するというワークアラウンドで対処しています。

===追記===
そもそもファイル添付時のメッセージではSlashコマンドが反応しないので無理そうでした…。
(何かいい方法があればおしえてください!)

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?