Google Apps Script で 勉強会通知 Slack Bot を作ってみた

Google Apps Script で 勉強会通知 Slack Bot を作ってみた

勤務先の所属グループでは、週次で技術ネタの社内勉強会を実施していて Google Sheets を使ってスケジュールの管理をしています。シートの閲覧・編集権限はメンバー各々が持っていて、発表希望者は自ら発表ネタを空いている日に記入し、当日に発表するという運営スタイルです。

翌週の発表者が埋まっていない場合は、 ミーティング や Slack で「次回の勉強会で発表したい方いませんかー?」とか聞いたりしていますが、都度スケジュールの埋まり具合を確認してメンションを飛ばすのはかなり面倒だったります。そこで、 下記を目的として Google Apps Script x Slack 連携をやってみました。

  • Slackへの勉強会通知を自動化して、 発表希望者を募りやすくする
  • GAS とか触ったことないから触って 社内勉強会のネタにする

つくった機能

  • Google Sheets に書かれた勉強会情報を取得する
  • 勉強会情報を Slack 通知用に整形する
  • 開始前に Slack チャンネルで通知する
  • 一週間前に Slack チャンネルで次回予告する

Google Apps Script (GAS) とは

Google Apps Script は Gmail, Calendar, Docs, Spreadsheets などの Google Apps を扱うための js ベースのスクリプト言語。 各 Google Apps のサービスにアクセスするための便利な API が用意されているので、拡張機能をつくったり、処理・操作を自動化などGoogle Appsをカスタマイズすることができます。

Spreadsheet Service で情報取得

まずは、チームメンバーが書き込んだスケジュール情報を GAS の Spreadsheet Service を使って取得します。

study session image

Spreadsheet Servcie

Spreadsheet Service は基本的に下記の4つの Class を使ってデータの操作をします。

  • SpreadsheetApp Class
    • Spreadsheet Service を使うための Class。 Google Sheets ファイルを開いたり、追加したり出来る。
  • Spreadsheet Class
    • スプレッドシートファイルを扱う Class。ファイル内のシートにアクセスしたり、ファイルの共有権限の設定をしたり出来る。
  • Sheet Class
    • スプレッドシートファイルのシート情報を扱う Class。Range情報を呼び出したり、シートの追加・変更ができる。
  • Range Class
    • シートの範囲を扱うクラス。 GASでは各セル毎のクラスは存在せず、範囲を表す Range を使ってセル操作を行う。
SheetLoader.gs

コードはこんな感じになります。 普段、当たり前のようにDB操作をしていると、シートからのデータ参照はかなりダルく感じました。。

"use strict";

var SPREAD_SHEET_ID = 'hogehoge_abcdefg';  
var SHEET_NAME = 'スケジュール';  
var USER_INPUT_RANGE = 'A4:G100';

// シートから勉強会情報を取得して配列で返してくれる君
function getStudySessions() {  
  var spreadSheet = SpreadsheetApp.openById(SPREAD_SHEET_ID);
  var userInputRange = spreadSheet.getRange(SHEET_NAME + '!' + USER_INPUT_RANGE);

  var studySessions = [];
  for (var i = 0; i < userInputRange.getNumRows(); i++) {
    var sessionRange = userInputRange.offset(i, 0, 1, userInputRange.getNumColumns() - 1);    
    var sessionValues = sessionRange.getDisplayValues();

    if (sessionValues[0][0]) {
      var studySession = {
        dateStr: String(sessionValues[0][0]),
        openAt: sessionValues[0][1],
        closeAt: sessionValues[0][2],
        roomNumber: sessionValues[0][3],
        speaker: sessionValues[0][4],
        title: sessionValues[0][5],
        documentUrl: sessionValues[0][6]
      };
      studySessions.push(studySession);
    }
  }

  return studySessions;
}

Incoming Webhooks で Slack に通知

次に GAS を使って Slack に通知するコードを書きます。 Slack の Channel に投稿するには Slack Web API, Incoming Webhooks を使う手段があります。
今回は簡単に投稿できるらしい Incoming Webhooks を使うことにしました。

Incoming Webhooks

Incoming Webhooks は 外部から Slack にメッセージを投稿するための Slack Integration。 JSON payload に message をのせて HTTP リクエストするだけで Slack に投稿ができます。

curl で試してみる
WEB_HOOK_URL="https://hooks.slack.com/services/hoge/fuga/foobaaaaaar"

curl -X POST \  
--data-urlencode 'payload={"text":"Hello Incoming Webhooks!", "channel": "@itomoyasu"}' \
$WEB_HOOK_URL

test post by curl image

SlackWebHook.gs

GAS で書くとこんな感じになります。

"use strict";

// GAS からテスト送信してみる
function callSlackWebhookTest(message) {  
  var params = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify({
      channel: "@itomoyasu",
      username: SLACK_USERNAME,
      icon_emoji: ":sushi_hanpuku:",
      text: "test webhook." + message
    })
  };

  var response = UrlFetchApp.fetch(SLACK_WEBHOOK_URL, params);
  Logger.log(response);
}

test post by GAS

attachments option

attachments option を使えばいい感じにメッセージを整形できるので、勉強会通知用の attachment を作りました。

SlackWebHook.gs

attachments option を指定して通知メッセージを整えたスクリプトはこんな感じになります。

"use strict";

var SLACK_ATTACHMENT_DEFAULT_FORMAT_COLOR = "#a9a9a9";// darkgray  
var SPREAD_SHEET_URL = "https://docs.google.com/spreadsheets/x/hogehoge_fugafuga";  
var SLACK_USERICON_URL = "https://a.slack-edge.com/66f9/img/icons/favicon-32.png";


// attachment 情報を使って Slack に Post する
function callSlackWebhookByAttachment(attachment, options) {  
  options = options || {};

  var params = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify({
      channel:    options.channel    || SLACK_TARGET_CHANNEL,
      username:   options.username   || SLACK_USERNAME,
      icon_emoji: options.icon_emoji || SLACK_ICON_DEFAULT_EMOJI,
      attachments: [ attachment ]
    })
  };

  var response = UrlFetchApp.fetch(SLACK_WEBHOOK_URL, params);
  Logger.log(response);
}


// studySession Obj から attachment 情報を作成
function createAttachment(studySession, options) {  
  options = options || {};

  var attachment = {
    fallback: studySession.title,
    color: options.color || SLACK_ATTACHMENT_DEFAULT_FORMAT_COLOR, 
    pretext: "<" + SPREAD_SHEET_URL + "|勉強会カレンダー>",
    author_name: studySession.speaker,
    author_icon: SLACK_USERICON_URL,
    title: studySession.title,
    title_link: studySession.documentUrl,
    text: (
      "日時: " + studySession.dateStr + " " + studySession.openAt + " ~ " + studySession.closeAt +
      "\n部屋: " + studySession.roomNumber
    ),
    thumb_url: options.thumb_url || '',
    short: false 
  };  
  return attachment;
}

Trigger を使った実行

最後に、「SpreadSheet からの情報取得」と「Slack に通知」する一連の処理を行う function を定義して GAS の Trigger を使って実行させます。

Time-driven(clock) Trigger

GAS の Trigger を使うと Open, Edit, Install, Change, Form submit, Time-driven (clock), Get, Post のイベントが発生したときに、任意の function 呼び出しを実行できます。イベントの種類については こちら にまとまっています。
今回は定期間隔で自動実行できる Time-driven Trigger を使います。

Trigger.gs

当日の勉強会通知を行う notifyTodaysSession() と 翌週の勉強会通知を行う notifyNextSession() を定義します。

"use strict";

var ONE_MINUTE = 1000 * 60;  
var ONE_WEEK = 1000 * 60 * 60 * 24 * 7;

var SLACK_START_IMAGE_URL     = "http://blog-imgs-46.fc2.com/s/u/n/sunuo7/sasakutteru.jpg";  
var SLACK_NEXT_WEEK_IMAGE_URL = "https://pbs.twimg.com/media/CXy3YECUoAARF46.jpg";  
var SLACK_ATTACHMENT_START_FORMAT_COLOR   = "#00bfff";// deepskyblue


// 勉強会当日の開始前に呼び出す Trigger
function notifyTodaysSession() {  
  Logger.log("Start notifyTodaysSession() at " + new Date());

  var studySessions = getStudySessions();

  var targetTerm = {
    startAt: Date.now(),
    endAt: Date.now() + 30 * ONE_MINUTE
  };

  studySessions.forEach(function(studySession) {  
    var openDateTime = Date.parse(studySession.dateStr + " " + studySession.openAt);

    if (targetTerm.startAt <= openDateTime && openDateTime <= targetTerm.endAt) {
      Logger.log("Found target study session: " + studySession.title);
      var attachment = _createAttachment(studySession, { color: SLACK_ATTACHMENT_START_FORMAT_COLOR, thumb_url: SLACK_START_IMAGE_URL });
      callSlackWebhookByAttachment(attachment);
    }
  });  
  Logger.log("Done");
}


// 勉強会一週間前に呼び出す Trigger
function notifyNextSession() {  
  Logger.log("Start notifyNextSession() at " + new Date());

  var studySessions = getStudySessions();

  var targetTerm = {
    startAt: Date.now() + ONE_WEEK - (30 * ONE_MINUTE),
    endAt: Date.now() + ONE_WEEK
  };

  studySessions.forEach(function(studySession) {
    var closeDateTime = Date.parse(studySession.dateStr + " " + studySession.closeAt);

    if (targetTerm.startAt <= closeDateTime && closeDateTime <= targetTerm.endAt) {
      Logger.log("Found target study session: " + studySession.title);
      var attachment = _createAttachment(studySession, { thumb_url: SLACK_NEXT_WEEK_IMAGE_URL });
      callSlackWebhookByAttachment(attachment);
     }
  });  

  Logger.log("Done");
}

Trigger を設定する

Time-driven Trigger の設定は Script Editor からポチポチ操作で設定できます。どちらの Trigger とも 30分間隔で定期実行されるように設定します。

setting of trigger

なお、動的に Trigger を発行したい場合は、 Trigger Builder Class を使って GAS で定義することも可能です。

以上で、 Google Sheets に記入された勉強会情報を読み込み Slack に通知を飛ばすことができます。

所感

  • Slack Incoming Webhooks 楽しい
  • 業務で Spreadsheet をよく使っているような場合だと、GAS を応用すれば業務の効率化ができるかもしれない
  • やっぱり DB は便利
    • Spreadsheet に Range でアクセスしてデータ引くのつらすぎる..
    • SpreadSheet からのデータ参照は、複雑なデータ構造のシステムだとすぐに破綻しそう
    • 実用的なものを作るなら、Excel力(データ設計能力)が問われる
  • Trigger Class は function 呼び出しに引数指定できないので使いづらかった
    • function 指定できれば、 Daily で SpreadSheet をパース => Slack 通知を必要な分だけ clock Trigger に登録するようなコードが書けるので、コードはきれいになりそう。

Tomoyasu Ishikawa

Read more posts by this author.