3
2

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.

【GAS】 slack に新規作成されたチャンネルを定期的に通知する

Last updated at Posted at 2019-10-07

新規チャンネルに気づけない・気づかれない

新しくチャンネルを作成してもワークスペースの全員に都度お知らせするのはなんだか差し出がましいような気がしてしまう…… そんなときは BOT に通知してもらえばいいのです。
20191072222625_1.png

事前準備

Event Subscription

アプリの Event Subscription を有効にして、 channel_created channel_archive channel_deleted のイベントを監視させます。
20191072222253.png

Response URL には後述する GAS への公開 URL を貼り付けます。チャンネル関連でイベントが発生すると、イベント内容を含んだ HTTP リクエストがこの URL に向けて送られます。

アプリの Scope を設定

チャンネルに投稿するための権限として chat:write:bot のスコープでアプリに権限を与えておきます。

GAS 側の処理

シートにチャンネルのログを残す

GAS に用意されている doPost() 関数を使用して、イベントによって送信された HTTP リクエストを受け取って処理します。
シートには イベント発生日時 イベントが発生したチャンネル名 チャンネル ID イベントの種類 の4列でログを残します。

var SHEET_ID = PropertiesService.getScriptProperties().getProperty("SHEET_ID");
var sht = SpreadsheetApp.openById(SHEET_ID).getSheets();

function doPost (e) {

  // リクエスト内容をパース
  var postData = JSON.parse(e.postData.getDataAsString());
  var contents = JSON.parse(e.postData.contents);
  
  // 認証( url_verification が送信された場合は challenge をオウム返しする)
  if(postData.type == "url_verification") {
    var res = {"challenge":postData.challenge}
    return ContentService.createTextOutput(JSON.stringify(res)).setMimeType(ContentService.MimeType.JSON);
  }
  
  // 無効な入力を弾く
  if (e == null || e.postData == null) {
    return;
  }
  
  // シートに加筆
  var eventType = contents.event.type;
  if (eventType == "channel_created") {
    var channelName = contents.event.channel.name;
    var channelID = contents.event.channel.id;
  }
  else {
    var channelName = "obsolete_channel";
    var channelID = contents.event.channel;
  }
  
  sht[0].appendRow([new Date(), channelName, channelID, eventType]);
  
}

ハマったポイント1:認証

イベントが発生すると、イベント内容が送信される前に url_verification という内容の JSON が送信され、そこに含まれている challenge を slack 側にオウム返ししてやる必要があります。この認証段階を経て初めてイベント内容に関するリクエストが飛んできます(公式ドキュメント)。

ちなみに認証が済んだあとも処理がタイムアウト(3秒)などすると slack は 最大3回 リトライしてくるようです。

We'll knock knock knock on your server's door, retrying a failed request up to 3 times in a gradually increasing timetable:

リトライ回避のためには slack へ返す HTTP ヘッダに X-Slack-No-Retry: 1 を指定すればいいそうなのですが GAS でそれを行う方法がわからず、シートに行を追加するだけならタイムアウトしないのをいいことに放置しています。

最終的に処理が終わった段階でなにも return していないのに特にリトライ処理されていない点など、色々気になり始めるとキリがないのですが とりあえず動いているので良しとします 今後検討していこうと思います。

ハマったポイント2:イベントによる JSON 形式の違い

:sob: 統一してほしい!

追加されたチャンネルの情報を取得する

function getNewChannel () {
  // 日付
  var now = new Date();
  var startOfToday = new Date(now.getYear(), now.getMonth(), now.getDate(), 00, 00);
  var startOfYesterday = new Date(now.getYear(), now.getMonth(), now.getDate()-1, 00, 00);
  
  // チャンネル
  var newChannel = [];
  //  削除・アーカイブされたもの
  var exception = [];
  
  // データ取得
  var maxRow = sht[0].getLastRow();
  var data = sht[0].getRange(1, 1, maxRow, 4).getValues();
  for (var r = maxRow - 1; r >= 0; r--) {
    // 2日以上前の場合は抜ける
    var addDate = new Date(data[r][0]);
    if (addDate.getTime() < startOfYesterday.getTime()) {
      break;
    }
    
    var info = "  <#" + data[r][2] + "|CHANNELNAME>"; // ID 優先でリンクが貼られるのでチャンネル名は適当
    if (data[r][3] == "channel_archive" || data[r][3] == "channel_deleted") {
      exception.push(info);
    }
    
    // 今日になってから追加されたチャンネルはスキップ
    if (addDate.getTime() > startOfToday.getTime()) {
      continue;
    }
    
    newChannel.push(info);
  }
  
  // 消されていないチャンネルだけ取得
  var ret = [];
  for (var i = 0; i < newChannel.length; i++) {
    if (exception.indexOf(newChannel[i]) == -1) {
      ret.push(newChannel[i]);
    }
  }
  return ret;
}

前日に作成されたチャンネルの ID を取得してから、実行時までに削除かアーカイブされたチャンネルを除外しています。

定期実行

これまでに書いた関数を組み合わせて下記の関数を作り、 GAS の時間トリガーで notifyNewChannel() 定期実行します。

var ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("ACCESS_TOKEN");
var CHANNEL_ID = PropertiesService.getScriptProperties().getProperty("CHANNEL_ID");

// slack に chat.postMessage 経由でメッセージを投稿する関数
function postAsBot (text) {
  var payload = {
    "token": ACCESS_TOKEN,
    "channel": CHANNEL_ID,
    "text": text
  };
  var options = {
    "method" : "POST",
    "payload": payload
  }
  UrlFetchApp.fetch("https://slack.com/api/chat.postMessage", options);
}

// 新規チャンネル情報を投稿
function notifyNewChannel () {
  var newChannel = getNewChannel();
  if (newChannel.length < 1) {
    return;
  }
  var msg = "昨日、" + newChannel.length + "個のチャンネルが作成されました!\n" + newChannel.join("\n")
  postAsBot(msg);
}

コード全体

全文表示
///////////////////////////////////////////////////////
// グローバル変数
///////////////////////////////////////////////////////

// シート
var SHEET_ID = PropertiesService.getScriptProperties().getProperty("SHEET_ID");
var sht = SpreadsheetApp.openById(SHEET_ID).getSheets();

// token
var ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("ACCESS_TOKEN");

// チャンネル
var CHANNEL_ID = PropertiesService.getScriptProperties().getProperty("CHANNEL_ID");

///////////////////////////////////////////////////////
// 応答処理
///////////////////////////////////////////////////////

function doPost (e) {

  // リクエスト内容をパース
  var postData = JSON.parse(e.postData.getDataAsString());
  var contents = JSON.parse(e.postData.contents);
  
  // 認証( url_verification が送信された場合は challenge をオウム返しする)
  if(postData.type == "url_verification") {
    var res = {"challenge":postData.challenge}
    return ContentService.createTextOutput(JSON.stringify(res)).setMimeType(ContentService.MimeType.JSON);
  }
  
  // 無効な入力を弾く
  if (e == null || e.postData == null) {
    return;
  }
  
  // シートに加筆
  var eventType = contents.event.type;
  if (eventType == "channel_created") {
    var channelName = contents.event.channel.name;
    var channelID = contents.event.channel.id;
  }
  else {
    var channelName = "obsolete_channel";
    var channelID = contents.event.channel;
  }
  
  sht[0].appendRow([new Date(), channelName, channelID, eventType]);
  
}

///////////////////////////////////////////////////////
// slack 関連
///////////////////////////////////////////////////////

// slack に chat.postMessage 経由でメッセージを投稿する関数
function postAsBot (text) {
  var payload = {
    "token": ACCESS_TOKEN,
    "channel": CHANNEL_ID,
    "text": text
  };
  var options = {
    "method" : "POST",
    "payload": payload
  }
  UrlFetchApp.fetch("https://slack.com/api/chat.postMessage", options);
}

///////////////////////////////////////////////////////
// 通知処理
///////////////////////////////////////////////////////

// 昨日追加されたチャンネルを取得
function getNewChannel () {
  // 日付
  var now = new Date();
  var startOfToday = new Date(now.getYear(), now.getMonth(), now.getDate(), 00, 00);
  var startOfYesterday = new Date(now.getYear(), now.getMonth(), now.getDate()-1, 00, 00);
  
  // チャンネル
  var newChannel = [];
  //  削除・アーカイブされたもの
  var exception = [];
  
  // データ取得
  var maxRow = sht[0].getLastRow();
  var data = sht[0].getRange(1, 1, maxRow, 4).getValues();
  for (var r = maxRow - 1; r >= 0; r--) {
    // 2日以上前の場合は抜ける
    var addDate = new Date(data[r][0]);
    if (addDate.getTime() < startOfYesterday.getTime()) {
      break;
    }
    
    var info = "  <#" + data[r][2] + "|CHANNELNAME>"; // ID 優先でリンクが貼られるのでチャンネル名は適当
    if (data[r][3] == "channel_archive" || data[r][3] == "channel_deleted") {
      exception.push(info);
    }
    
    // 今日になってから追加されたチャンネルはスキップ
    if (addDate.getTime() > startOfToday.getTime()) {
      continue;
    }
    
    newChannel.push(info);
  }
  
  // 消されていないチャンネルだけ取得
  var ret = [];
  for (var i = 0; i < newChannel.length; i++) {
    if (exception.indexOf(newChannel[i]) == -1) {
      ret.push(newChannel[i]);
    }
  }
  return ret;
}

// 新規チャンネル情報を投稿
function notifyNewChannel () {
  var newChannel = getNewChannel();
  if (newChannel.length < 1) {
    return;
  }
  var msg = "昨日、" + newChannel.length + "個のチャンネルが作成されました!\n" + newChannel.join("\n")
  postAsBot(msg);
}
3
2
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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?