Slackに暗示的なリマインダが欲しい

 これはKCS Advent Calendar 2020 21日目の記事です。

20日目 | 22日目→

 こんにちは、2年のkkrrです。

 この記事では、誰でも手軽にSlackのリマインダを作ることができる一つの方法を紹介しようと思います。始めに謝罪しておきますが、内容の薄さに反して記事が少し長くなってしまいました。また、素人がちょっとした思いつきで何も知らないところから5時間で作ったレベルなので、クオリティには目を瞑って温かい目でご覧ください。(言い訳です。)

 

背景

 大学生になるとサークルや学科などでSlackを使用する機会が増えるのではないでしょうか。その際、特定の日時に特定のメッセージを送るような予約投稿ができれば便利だというシーンがあるはずです。例えば何らかのイベントの直前に概要を通知したりZoomのリンクを送ったり、何らかの〆切の直前に警告文を送ったり等、要するにリマインド機能です。

 折角の機会なので何か記事を書こうと思いつつ、今年自分がKCSでやってきたこと(PRML, Kaggleとか)に関しては上位互換が多すぎて書ける気がしなかったので、ちょっとした思いつきでSlackのリマインダを自分なりに作ってみることにしました。

 

従来手法の課題

 Slackにはデフォルトでリマインド機能が存在します。詳細は公式に委ねますが、以下のようなコマンドを打つことでそこそこ簡単に設定できます。

/remind [@メンバー/#チャンネル] [内容] [時間]

 これで十分と言えばそれまでですが一つ大きな課題を挙げるとすれば、チャンネルにリマインド設定したことが表示されてしまうことではないでしょうか。例えば以下のように設定する場合を考えます。(3/15は適当です。)

すると以下のように、設定したチャンネルに投稿予定日時と内容がそのまま表示されてしまいます。

実際にこんな使い方をする機会があるかは知りませんが、これではサプライズに使えず興醒めしてしまいますよね?

たとえ「何時からミーティングが始まります」くらいの内容だとしても、設定した瞬間に設定した内容が表示されてしまうのは何となく格好が悪い感じがします。おまけに、通常のメッセージと同様に通知が行ってしまいます。

 あえてもう一点課題を挙げるすれば、人によってはコマンドが覚えづらいと感じるかもしれないことでしょうか。わざわざこの記事を読んでくださっている方にそのような人は居なさそうですが、コマンド入力に少し抵抗がある人も世間には一定数居ることでしょう。

 

作ったもの

 さて、前置きが長くなってしまいましたがようやく本題に入ります。先に挙げた2点の課題を解決できそうなものを作ってみたいと考えました。Slackとは便利なもので大体のツールは先人により既に開発されていますが、勉強のためにあえて自分でやってみたい、自分のイメージにドンピシャのものが見つからなかったというモチベーションで臨みました。(もっと高機能なものはいくらでもありそうですがシンプルで手軽なものが見つかりませんでした。)

 

使ったもの

 以下の3つです。

  1. Slack (Incoming Webhook)
  2. Google スプレッドシート
  3. Google Apps Script

 1つめは当然ですがSlackです。Slackには外部サービスと連携する機能が多く存在しますが、Incoming Webhookを導入することで外部からHTTPリクエストを送るだけで簡単にメッセージを送信することができます。公式はこちら

 2つめはGoogle スプレッドシートです。ここに、送信日時、送信先、送信内容を登録することで自動でリマインドがなされるようにします。後程説明しますが、セルに各項目を記入するだけなのでコマンドより分かりやすくなるのではないかという安易な理由と、個人的な好みで選びました。

 3つめはGoogle Apps Script、所謂GASです。名前だけ知っていてずっと気になっていたので今回使ってみることにしました。GASはJavaScriptをベースにしたGoogleの提供するプログラミング言語です。スプレッドシート他Google提供サービスとの連携が容易であること、Googleのサーバ上で動くため環境構築が不要なこと、基本的に無料であることなど多くの利点があります。

   

概要

 全体の構成は以下のようにしました。

 リマインド設定をしたい内容を予めスプレッドシートに入力しておくと、指定した時間になったことをGASが確認してSlackに伝達してくれる感じです。

 

機能

 大まかな使用手順を示します。4工程あります。

 

①人間がスプレッドシートに登録

 リマインドしたい日付、時間、送信先チャンネル、本文を以下のように記述していきます。一行が一件のリマインドに対応します。何をどの順番に書けば良いかはコマンドより分かりやすそうです。時間は分単位で指定可能で、送信先は通常のチャンネルの他、個人宛てにもできます。本文の欄には以下の例で記述したような工夫が可能です。(変な時間に記事を書いていることがバレますね…)

 

②GASがリマインド設定を確認

 1分おきに以下の関数が実行されます。リマインド設定時間を分単位で指定するためには必要なことです。とりあえず動けば精神でかなり愚直な実装になりました。JSを初めて書いたのでコーディング規約違反的なものがあっても今回は見逃してください…

(どうでも良いですが、コードの冒頭と終末にpタグが勝手に表示されてしまうのはなぜなんでしょうか?あと、&の後にamp;が勝手についてしまうみたいですが気にしないでください。)

</p>
function checkReminder() {
  // spreadsheet
  let spreadSheet = SpreadsheetApp.openById('');  // スプレッドシートのIDを指定(ここでは省略)
  let upcomingSheet = spreadSheet.getSheetByName('upcoming');  // 取得するシート名を指定
  
  // date and time
  let now = new Date();
  let timeZone = 'Asia/Tokyo';
  let dateFormat = 'yyyy-MM-dd';
  let timeFormat = 'HH:mm';
  let today = Utilities.formatDate(now, timeZone, dateFormat);
  let currentTime = Utilities.formatDate(now, timeZone, timeFormat);
  
  // check remindlist
  let remindList = upcomingSheet.getDataRange().getValues();
  for(let i=1; i<remindList.length; i++){
    let remindDate = Utilities.formatDate(remindList[i][0], timeZone, dateFormat);
    let remindTime = Utilities.formatDate(remindList[i][1], timeZone, timeFormat);
    if(remindDate == today &amp;&amp; remindTime == currentTime){
      let channel = remindList[i][2];
      // replaceAllを使えないのが厄介
      for(let subi=0; subi<channelSub.length; subi++) channel = channel.split(channelSub[subi][0]).join(channelSub[subi][1]);
      let text = remindList[i][3];
      for(let subi=0; subi<textSub.length; subi++) text = text.split(textSub[subi][0]).join(textSub[subi][1]);
      sendMessage(channel, text);
    }
    // 日付同じかつ時間が後のリマインドが多いと厄介だが基本大丈夫
    else if(remindList[1][0].getTime() > now.getTime()) break;
  }
}

<p>

 スプレッドシートのIDやシート名は適宜使いたいものに指定してください。シート名というのは↓のことです。(因みにリマインド済みの項目を自動でpastシートに移動する処理を書こうと思っていたのですが時間が無かったので割愛しました…)

 関数実行日と記入された日付を比較するところで日付のフォーマットでくじけそうになり力技の実装になってしまいました。絶対にもっとキレイな書き方があります。

 取得したテキストを置換する処理の際に以下の配列を使用しています。なぜこんなことをしているのかと言うと、メンションを付けたいときにSlack上と同様に記述してもメンションにならないからです。Slackの表示名とユーザ名との不一致やSlackの仕様の問題です。

</p>
const channelSub = [
  // メンバーを予め設定しておく
  ['@kkrr', '@kkrr10'],
];
  
const textSub = [
  ['@channel', '<!channel>'],
  ['@here', '<!here>'],
  // メンバーを予め設定しておく
  ['@kkrr', '<@kkrr10>'],
  ['@ics', '@ics\n(<@kkrr10>, ...)']
];
<p>

 その他色々調整をしていますがまだまだ不十分なのは承知です。特に例外処理。

 1分おきに実行していると先に述べましたが、GASでは以下のようなGUIで簡単に関数を自動実行するトリガー設定が可能です。あまりにも簡単すぎておもちゃかと思いました。

 さて、1分おきに実行するということは1日に1440回も実行することになりGASの利用制限に引っかからないか心配になるかもしれません。しかし、G Suite Educationのアカウントを持つ学生はトリガーの総実行時間が 6時間 / 日 、URLフェッチのコール数が 100,000回 / 日 、スクリプト実行時間が 30分 / 実行 までなので全く問題ありません。GASの利用制限についてはこちらを参照しました。

 因みに今回の場合1分おきに実行するので、当然1分以内に処理が終わる必要があります。ログを確認してみると大体1秒前後で実行できていました。↓(登録済みリマインド数をNとしてO(N)の単純な実装に見えますが、リマインドがシート上で時系列順にソートされている状態に保つようにすれば実用レベルにはなるはずです。)

 このようなプログラム実行がGoogleのサーバーで無料でできるのですから使わない理由はないですね。結論としては、Google万歳!

   

③GASがIncoming WebhookへHTTP POST

 Incoming Webhook側の設定でWebhook URLが発行されるのでそれを指定してPOSTします。先の載せたcheckReminder関数内で呼び出されているsendMessageという関数で送信を行っています。以下のような関数です。

</p>
function sendMessage(channel, text) {
  let POST_URL = '';  // Webhook URLをここで指定(ここでは省略)
  
  let jsonData =
  {
    "channel" : channel,
    "text" : text,
  };
  let payload = JSON.stringify(jsonData);

  let options =
  {
    "method" : "post",
    "contentType" : "application/json",
    "payload" : payload
  };

  UrlFetchApp.fetch(POST_URL, options);
}

<p>

 メッセージが送信されるときのユーザ名やアイコンなどもここで指定できますが、今回は使っていません。これらのプロパティはIncoming Webhook側でデフォルト設定が可能なのでそうしています。個人で使うときは好きなキャラクターに設定しましょう!

 今回は、「アンコ・バイナリー」というキャラクターを使わせていただくことにしました。話題がそれますが、アンコ・バイナリーとは理工学部情報工学科(ICS)の公式キャラクターであり、情報の海を照らし旅をする電子ちょうちんあんこうの妖精です。ライセンス表示を適切に行えば自由に使えるとのことです。

アンコ・バイナリー
By Ayumi & Yuri
クリエイティブ・コモンズ・ライセンス

もっと知名度が上がると良いですね。(?)

 

④Slackで通知

 工程①で載せたスプレッドシートの記述の通りに実行を待つと以下のようになります。5分間でこんなにメッセージを送るのは迷惑なのでやめましょう…(因みにこのワークスペースやアカウントは実験用に作成したものなので誰にも迷惑をかけていません。)

このように、個人宛てのメッセージはSlackbotから届きます。

 さて、実際に送信された5つのメッセージを確認してみましょう。

1つめ

replaceが上手く動作しています。

2つめ

絵文字も送信できていますが、絵文字の名前を覚えている人は少ないと思うので実用的ではない気もします。本記事は理工学基礎実験からの現実逃避として作成されました。

3つめ

Google Driveとかと連携したら画像を表示できるようになるかもしれないです。

4つめ

メンションのエイリアスが可能になります。実験用のワークスペースなので一人しか居ませんが、班活動などがあれば便利かもしれませんね。Discordと違ってSlackにはロールが存在しないので。

5つめ

よく考えたら誰が設定したか分からないメッセージが突然個人宛てに届くのは怖いですね。

 全て指定した時間通りに送信できています。以上で、とりあえず当初の目標は達成されたということにしたいと思います。

    

残された課題

 適当に作ったのでまだまだ実用には課題が多く残されています。何となく思いついた項目をリストアップしておきます。

  1. リマインド済みのメッセージをスプレッドシートから自動削除
  2. 項目記入不足の場合など、全体的な例外処理
  3. 画像を送れるようにする
  4. 誰が設定したリマインドなのか自動でメッセージに反映されるようにする
  5. 送信オプションとしてユーザ名やアイコンも設定できるようにする
  6. Slackのremindコマンドのように、特定時間や曜日に定期実行できるような設定を可能にする
  7. GAS→Slackの一方通行ではなくSlackからリマインドを追加したりリスト一覧を確認できるようにする(根本的な改変が必要…)
  8. 暗示とか言っておきながら誰でもスプレッドシートを閲覧できたら結局意味無くないですか?(リマインドの追加をフォーム送信形式にしてスプレッドシートを直接閲覧できないようにする?)

 色々欠点はありますが、今後自分の思うように仕様をいじることができるという点で、デフォルトのリマインド機能よりある程度優位性が存在するのではないかと勝手に自分を納得させています。

 

今後の展望

 単なるリマインダーだと面白くないのでもう少し役に立ちそうなもの、例えば複数人のタスクの進捗管理ができるようなものを作ってみたいです。〆切前日や半日前、1時間前になっても完了していない人に対して自動でリマインドしてくれるイメージです。

 今回やったことを基礎にすれば他にも色々応用の幅はありそうだと感じました。

 

最後に

 もしここまで読んでくださった方がいれば、最後までお付き合いいただきありがとうございました。いずれはこんなに簡素なものではなくちゃんとアプリの形をしたものを時間をかけて作れたら良いと願っています。今回のお遊びを通してGASの便利さを知ることができたので、GASのプロが居たら是非教えてください。

20日目 | 22日目→

 

Posted on: 2020年12月21日, by :