週番アプリの作成 - Slack を用いたクラス運営(5)

1. はじめに

産技高専では号令やゴミ捨てなどを週ごとに担当する週番が設定されている。学生が欠席することなどもあるため、1週間に3人割り当てることがデフォルトになっている。几帳面な担任の場合、年度当初の行事日程などに週番を書き込んだりして割り当てるが、私のようにズホラな担任は学生任せにしてしまうこともある。そのような場合にはホワイトボードなどで学生が管理するのだが、うまく更新されない場合が多い。そこで、昨年度は週番を自動更新するアプリを作成した(ベースはうちの卒研生が作ったのだが、かなりアクロバティクな方法で更新していたので、中身をかなり簡潔な表現に作り替えた)。

2. 作成するアプリ

週番は1週間ごとに3人を割り当てている。 今回作成したアプリは、これを自動的に投稿するように作成する。 テストのために、以下のコマンドを追加で作成する。

  • /shuuban now: 現在の週番を表示する。
  • /shuuban メッセージ: メッセージを週番に送るためのテキストを作成する。
  • /shuuban start: 自動更新を開始する。
  • /shuuban stop: 自動更新を止める(長期休業中など)
  • /shuuban gomi: ゴミが溜まっていることをチャンネルにお知らせする。
  • /shuuban next: 次の週番に切り替えて表示する(デバッグ用)。
  • /shuuban prev: 前の週番に戻して表示する(デバッグ用)。
  • /shuuban help: これらのコマンドの説明を表示する。

アプリは Google Apps Script (以下、GASと略)で作成し、データ自体は Google Spreadsheet に保存する。Slack の API を GAS から呼び出すことで Slack に投稿する。

3. アプリの作成

以下、アプリ作成手順を示す。スクリーンショットはアプリケーション作成当時の2019年度の3300クラスのものである。

3.1 Slack 投稿の設定

  • テスト用のチャンネル「テストスペース」を用意する。学生に見えないようにプライベートチャンネルにするとよい
  • slack api にアクセスし、「Start building」ボタンをクリックする。
  • アプリ名「Shuuban」を記載し、利用するワークスペースを選択し、「Create App」をクリックする。

    f:id:hkob:20200517132943p:plain
    Create Slack App

  • アプリが作成できたら、Features の Incoming Webhooks を On にする。 投稿するチャンネルは先ほど作成した「テストスペース」とする。

    f:id:hkob:20200517133159p:plain
    Incoming Webhooks

  • 設定が正しくできたかどうかは、「Sample curl request to post to a channel:」に書かれたコマンドを実行してみれば確認できる。curl コマンドで指定された URL に POST することで、「Hello, World!」とチャンネルに投稿できれば成功である。

curl -X POST -H 'Content-type: application/json' --data '{"text":"Hello, World!"}' 「URL」

3.2 Google Spreadsheet の設定

  • Google Spreadsheet に以下のようにデータを入力する。

    • Spreadsheet の名前は Shuuban とする。
    • A1 には自動、B1 には 1 を入力する。B1 が 1 の時は月曜日に週番が更新される。
    • A2 から下に学生番号、B2 から下に学生の氏名を入力する。A2 から B4 までが現在の週番である。この部分のデータをローテーションすることで週番の交代を行う(図の例では 3329 から 3331 までが今週の週番)。
    • 図には出ていないが、C2から下にメールアドレスの@の前の部分を記載する。これは後でメンションに活用する。
      f:id:hkob:20200517133744p:plain
      スプレッドシートの内容
  • この Google Spreadsheet に Google Apps Script を連携させる。まず、「ツール」メニューの「スクリプトエディタ」を開く。

    f:id:hkob:20200517133856p:plain
    スクリプトエディタの起動

  • コード.gs に以下のようにスクリプトを書いた。各関数の内容は以下の通り。

    • postSlack(text): text を指定したチャンネルに投稿する。Post URL は上で作成した URL。
    • doPost(e): Slack のコマンドから呼び出される。ここでコマンドの引数を調べ、対応する関数を呼び出す。help 以外は結果をチャンネルに投稿する。help は個人の画面のみにテキストを表示するようにした。
    • getSheet(): スプレッドシートのアクティブシートを得るためのコンビニエンス関数。他の関数から呼び出される。
    • getMembers(text): テキストとともに現在の週番を表示する
    • nowShuuban(): 現在の週番を表示する
    • nextShuuban(): 現在の週番を一番下に移動し、抜けた3行を削除する。その後、新しい週番を表示する。
    • prevShuuban(): nextShuuban で下に移動してしまった週番を上に戻し、その後、元の週番を表示する。
    • changeAuto(newValue): 自動更新のオン・オフを newValue で設定する。
    • startAuto(): 自動更新をオンにする。
    • stopAuto(): 自動更新をオフにする。
    • autoNextShuuban(): 自動更新がオンの時に、週番を更新する。月曜日に定期的に呼び出される。
function postSlack(text){
//  var url = "https://hooks.slack.com/services/----------/-------";//WebhookURL
  var url = "https://hooks.slack.com/services/--------/-------"; // test space
  var options = {
    "method" : "POST",
    "headers": {"Content-type": "application/json"},
    "payload" : '{"text":"' + text + '"}'
  };
  UrlFetchApp.fetch(url, options);
}

function doPost(e) {
  var command = e.parameter.text;
 
  if (/next/i.test(command)) {
    nextShuuban();
  } else if (/prev/i.test(command)) {
    prevShuuban();
  } else if (/gomi/i.test(command)) {
    postSlack("ゴミが溜まっています。処理をお願いします");
  } else if (/stop/i.test(command)) {
    stopAuto();
  } else if (/start/i.test(command)) {
    startAuto();
  } else if (/now/i.test(command)) {
    nowShuuban();
  } else if (/help/i.test(command)) {
    var message = ["/shuuban now: 現在の週番を表示します(3300)。",
                   "/shuuban next: 強制的に次の週番に変更します。",
                   "/shuuban prev: 強制的に前の週番に変更します。",
                   "/shuuban gomi: ゴミが溜まっていることを週番に報告します。",
                   "/shuuban start: 週番の自動更新を開始します。",
                   "/shuuban stop: 週番の自動更新を終了します(長期休業中など)。",
                   "/shuuban コメント: 週番にコメントメンションを送るためのテキストを作成します。"
                  ].join("\n");
    return ContentService.createTextOutput(JSON.stringify({text: message})).setMimeType(ContentService.MimeType.JSON);
  } else{
    return ContentService.createTextOutput(JSON.stringify({text: comment(command)})).setMimeType(ContentService.MimeType.JSON);
  }
  return ContentService.createTextOutput("");
}

function getSheet() {
  return SpreadsheetApp.getActiveSheet();
}

function comment(text) {
  var sheet = getSheet();
  var values = sheet.getSheetValues(2, 3, 3, 1);
  return ("次の行をコピーしてメッセージを送ってください。\n" + values.map(function( value ) {
    return "@" + value + ' ';
  }).join('') + text);
}

function getMembers(text) {
  var sheet = getSheet();
  var values = sheet.getSheetValues(2, 1, 3, 2);
  postSlack(text + "\n" + (values.map(function( value ) {
    return value.join(' ');
  }).join("\n")));
}

function nowShuuban() {
  getMembers("現在の週番を表示します。");
}

function nextShuuban() {
  var sheet = getSheet();
  var lastrow = sheet.getLastRow();
  sheet.getRange("A2:C4").moveTo(sheet.getRange("A"+(lastrow+1)));
  sheet.deleteRows(2, 3);
  getMembers("週番を交代します。");
}

function prevShuuban() {
  var sheet = getSheet();
  var lastrow = sheet.getLastRow();
  sheet.insertRowsAfter(1, 3);
  sheet.getRange("A"+(lastrow+1)+":C"+(lastrow+3)).moveTo(sheet.getRange("A2"));
  getMembers("週番を元に戻します。");
}

function changeAuto(newValue) {
  var sheet = getSheet();
  var cell = sheet.getRange("B1");
  var autoFlag = cell.getValue();
  if (autoFlag == newValue) {
    postSlack(["すでに自動更新は止まっています。", "すでに自動更新は動いています。"][autoFlag]);
  } else {
    cell.setValue(newValue);
    postSlack(["自動更新を止めます。", "自動更新を開始します。"][newValue]);
  }
}

function startAuto() {
  return changeAuto(1);
}

function stopAuto() {
  return changeAuto(0);
}

function autoNextShuuban() {
  var sheet = getSheet();
  var autoFlag = sheet.getRange("B1").getValue();
  if (autoFlag == 1) {
    nextShuuban();
  }
}
  • 作成した関数がうまく動くかを確認するために、実行したい関数を選択して再生ボタンを押してみる。以下の関数は直接テストできる。

    f:id:hkob:20200517135411p:plain
    コードの手動実行

    • nowShuuban(): 現在の週番がポストされるかを確認
    • nextShuuban(): 次の週番に変更になり、さらにそのことがポストされるかを確認
    • prevShuuban(): 前の週番に戻され、さらにそのことがポストされるかを確認
    • startAuto(): B1 のセルが 0 の時には 1 に変更され、自動更新オンがポストされるかを確認。すでに 1 の時にはその旨が表示されるかを確認
    • stopAuto(): B1 のセルが 1 の時には 0 に変更され、自動更新オフがポストされるかを確認。すでに 0 の時にはその旨が表示されるかを確認
    • autoNextShuuban(): 自動更新がオンの時に、週番が更新されるかを確認。オフの時に何もしないことを確認
  • 作成したスクリプトをウェブアプリケーションとして導入する。ここで、スクリプトを修正したら必ず「New」として新しくデプロイし直すこと。更新しても Web アプリケーションの URL は変更とならない。この後表示される ウェブアプリケーションの URL は別途記録しておく。

    f:id:hkob:20200517135839p:plain
    スクリプトのデプロイ

  • また、Slack との連携前に GAS 側で定時トリガーを設定してしまう。編集メニューにおいて、現在のプロジェクトのトリガーを表示する。設定した値は以下の通り。

    • 実行する関数を選択: autoNextShuuban
    • デプロイ時に実行: Head
    • イベントのソースを選択: 時間主導型
    • 時間ベースのトリガーのタイプを選択: 週ベースのタイマー
    • 曜日を選択: 毎週月曜日
    • 時刻を選択: 午前 7 時から 8 時
      f:id:hkob:20200517140122p:plain
      トリガの設定

以上で GAS 側は終了である

** 3.3 Slack のコマンド設定 * slack api にアクセスし、Shuuban アプリを表示後、左にある「Slack Commands」を選択する。 * 「Create New command」をクリックし、下図のように設定する(ただし、以下の画面はその後の編集画面である)。ここで、Request URL は先ほど GAS で記録しておいた URL である。

f:id:hkob:20200517140454p:plain
Slack コマンドの設定

  • 保存をすると、/shuuban コマンドが実行できるようになっているはずである。テストスペースで確認してみる。

3.4 学級日誌チャンネルへの変更

このままでは週番の交代はテストスペースに常に表示されてしまう。このため、学級日誌のページにこれらを表示するように変更する必要がある。以下の手順でポスト先を変更した。

  • Slack api の Incoming Webhooks で「Add New Webhook to Workspace」をクリックし、学級日誌チャンネルを選ぶ。
  • GAS の PostSlack にある URL をここで作成された Webhook URL に差し替える。これで投稿先は、変更された。
  • GAS を更新するために、再度ウェブアプリケーションとして導入を実行し、新規にデプロイする。

4 おわりに

これらの作業により、週番の更新が自動化された。 後は長期休業前に「/shuuban stop」、後期が始まったら「/shuuban start」するだけでよい。 また、週番がゴミ捨てをしていないようだったら、クラスの誰かが「/shuuban gomi」とタイプすればよいようになっている。 ただ、学級日誌を Slack にしてから、清掃報告はかなりちゃんと行われていて、このコマンドを使わなければならない時がほとんどないようだ。

hkob.hatenablog.com