ゆふてっく。

ソフトウェアテスト界隈の話を書きます多分。

Google Apps Script で connpass から特定グループのイベント情報を定期取得して、更新があったら Slack に通知するようにした話

タイトルの通りです。
とくに難しいことや変わったことはしてないのですが、せっかくなので書き残しておきます。

目的

  • connpass でテスト系のイベントが公開されたら、早めに気付きたい
    (現状は「なんとなくTwitterで流れてきて知る」とかなんだけど、もっと適切に情報収集したい)
  • 「Slackに何かを自動通知」とか、世間では一般的だけど自分はやったことないのでやってみたい

使ったもの

やったこと

概要

  1. 予め、以下データをスプレッドシート上で定義しておく
    • 自分が興味ありそうなイベントを開催するグループの グループID の一覧
    • 前回取得時の最新イベントの 最終更新日時
  2. connpass APIを使い、上記グループ群のイベントを取得する (最終更新日時順)
  3. 取得したイベントの中に、「前回取得時より後に更新されたイベント」があったら以下を行う
    • Slack にイベント内容をPOSTする
    • スプレッドシートの「前回取得時の最新イベントの 最終更新日時」を更新する

具体的な内容

Google スプレッドシート

こんな感じのシートを作ります。
C列のグループIDを増やせば、検索対象のグループを増やせます。 (B列のグループ名は実際は使いませんが、人間が見て分かりやすくするために書いてます)

f:id:YUFU:20190503152544p:plain
スプレッドシートの内容

グループIDの調べ方

connpass のAPI仕様を見ると以下の記述があるのですが、

URLが https://connpass.com/series/1/ のグループの場合、グループIDは 1 になります。

現状(2019/05/03現在)だと、グループのTOPページのURIhttps://{グループ名?}.connpass.com/ になっています。
なので、「connpass APIに対し、グループ名か何かでイベント情報を検索 => レスポンスに含まれてるグループID を調べる」という とてもめんどくさい作業をしました。。

Google Apps Script

こんな感じです。
とりあえず動くので、まぁ今回の用途ではそれでいいかなーって思ってます。
(ていうか誰かがレビューしてくれる訳じゃないので、現状自力ではこれ以上直せない)

/////////////////////////////////////
// 定数
/////////////////////////////////////

// データ保存用のスプレッドシート
var SHEET = SpreadsheetApp.openById("★スプレッドシートのID★").getActiveSheet();

// 最新イベントの最終更新日時セル
var DATE_RANGE = SHEET.getRange(2, 1);

// connpassのイベント検索用URI
var CONNPASS_BASE_URI = "https://connpass.com/api/v1/event/";

// Slackの投稿用URI
var SLACK_URI = "https://hooks.slack.com/services/★省略★";


/////////////////////////////////////
// connpass関連
/////////////////////////////////////

// 検索対象のグループID群を取得する
function getGroupIds() {
  var groupIdRange = SHEET.getRange(2, 3, SHEET.getLastRow()-1);
  var groupIds = groupIdRange.getValues();

  return groupIds;
}

// 検索クエリを作る (グループIDで検索)
function createSearchQueryByGroupIds(groupIds, count) {
  return '?series_id=' + groupIds.join();
}

// 指定したグループのイベントを取得する
function getEvents(uri) {
  var response = JSON.parse(UrlFetchApp.fetch(uri).getContentText());

  return response.events;
}

// 新しく更新されたイベントを抽出する
function isUpdatedEvent(event) {
  var eventUpdatedTime = Date.parse(event.updated_at);
  var latestEventUpdatedTimeAtLastTime = Date.parse(DATE_RANGE.getValue());

  return eventUpdatedTime > latestEventUpdatedTimeAtLastTime;
}


/////////////////////////////////////
// Slack関連
/////////////////////////////////////

// Slack投稿用テキストを作成する
function createPostText(event) {
  var startDate = new Date(event.started_at);
  var startDateText = startDate.toLocaleString("ja-JP");

  var text =
      ":new: *" + event.title + "*" + "\n\n" +
        "- [Date] " + startDateText + "\n\n" +
          "- [Address] " + event.address + "\n\n" +
            "- [URI] " + event.event_url;

  return text;
}

// SlackにPOSTする
function postSlack(text) {
  var data = {
    "text": text
  };

  var options = {
    "method": "post",
    "contentType": "application/json",
    "payload": JSON.stringify(data)
  };

  UrlFetchApp.fetch(SLACK_URI, options);
}


/////////////////////////////////////
// スプレッドシート関連
/////////////////////////////////////

// 最新イベントの最終更新日時をスプレッドシートに保存する
// (次回の検索時、今回の最新イベントより新しいイベントのみを取得するため)
function saveLatestEventUpdatedDate(events) {
  var latestEvent = events[0];
  DATE_RANGE.setValue(latestEvent.updated_at);
}


/////////////////////////////////////
// main
/////////////////////////////////////

function notifyEvents() {
  var groupIds = getGroupIds();

  var searchQuery = createSearchQueryByGroupIds(groupIds);
  var searchUri = CONNPASS_BASE_URI + searchQuery;

  var updatedEvents = getEvents(searchUri).filter(isUpdatedEvent);

  if (updatedEvents.length > 0) {
    updatedEvents.forEach(function(event) {
      postSlack(createPostText(event));
    });

    saveLatestEventUpdatedDate(updatedEvents);
  }
  
  Logger.log(updatedEvents);
}
Slackの投稿用URIの調べ方

こちらのブログを参考にさせていただきました。 vaaaaaanquish.hatenablog.com

定期実行の設定

[G Suite Developer Hub][(https://script.google.com/home) で、作成したプロジェクトのメニューボタンからトリガーの設定するだけ。

f:id:YUFU:20190503153729p:plain
トリガー設定

ハマったところ

デバッグ実行中に undifined な変数にアクセスしちゃったときの動きが謎

undifined な変数にアクセスしちゃったとき、フツーに実行してればこんな感じで ReferenceError: 「{変数名}」が定義されていません。 ってエラーが出るのですが。

f:id:YUFU:20190503152701p:plain
実行時のエラー

デバッグ実行だと、エラーが出ずにそのまま進んでしまいます。

で、ループ処理とかしてると、こんな感じで画面下部の変数状態表示?が 何だか変になってきます (何故か arrayA[5][5][5] みたく階層化?してくけど、中身表示は [0, 1, 2, 3, 4] のまま)。

f:id:YUFU:20190503152743p:plain
デバッグ実行時

もしかしたらわたしに知識が無いから「何だか変」って思うだけで、実際は正しい動きなのかもしれませんが…とりあえず、基本的にデバッグ実行しながら進めてたので超ハマりました。。 (エディタの補完がないので、しょっちゅー変数名をTypoって あちこちで undifined になってた。。)

感想

わたしは普段、 IntelliJJava から Selenium 使って E2Eテストを書いたり、それを Jenkins で定期実行したりしてます。
それと比較すると、いろいろ勝手が違って印象的でした。

定期実行めちゃくちゃ簡単すごい

GASを選んだ理由(初めて使った)が「スクリプトの定期実行が簡単らしいから」なんですが、本当にスーパー簡単でビックリしました。
まさかのGUIから選択肢選ぶだけ。。しかもメンテフリー&無料。。(゚Д゚ )

Web/スプレッドシートとの親和性すごい

HTTPアクセスしたり、スプレッドシートの値を取得したり書き込んだりがめちゃくちゃ簡単でビックリしました。
UrlFetchApp.fetch(uri) の一行だけでレスポンス取ってこれるとか衝撃です。

動的型付け(?)ムズい

うっかり text型と number型を比較しちゃったりしても全然エラーが出ないので、細かいミスに気付きにくくて辛かったです。
また、関数宣言に戻り値の型が書かれてる状況に慣れてるせいか、パッと見で「こいつ何返すんだっけ? むしろ返さないんだっけ??」と混乱することが多かったです。
(とくに関数の分割方針や内容を試行錯誤してる最中など、関数側を戻り値なしに変更したのに、呼び出し側でそれに対応するのを忘れたり…)

変数や関数の名前の付け方が悪いのかもしれないですが、やっぱり明示的に書いてある方が把握しやすくていいなぁと思いました。

IDE(InteliiJ)すごい

今回Web上のGAS用エディタでスクリプト書いたのですが、いかに日々 IntelliJ に助けられてるか心底痛感しました。
間違ったこと書いたらその場で教えてくれるし、そもそも補完に出てこないから書く前に「自分がやろうとしてたこと間違ってた」って気付いたりするし。。

間違いは絶対に発生するので、「いかに早く間違いに気付かせてくれるか」ってめっちゃ大事なんだなーーーだからテスト自動化ってありがたいんだなーーーー…と しみじみ思いました (むりやり本業に繋げてくスタイル)。

おまけ・検討したけどダメだったアプローチ達

イベント開催告知ツイートを検索 => Slack に通知

最初、IFTTT の Use Twitter Advanced Search to post to Slack Channel という Applet を使って、「Twitter Advanced検索でイベント告知っぽぃツイートを見つけたら Slack に POST する」というアプローチを試しました。
ちなみに検索クエリは connpass.com テスト OR QA OR 品質 -申し込みました! です。

ただ、これだと「今日はこのイベントに参加します!」みたいなツイートも拾ってしまい、ノイズが多いのでやめました。

キーワードでイベント検索

connpass のイベントサーチAPIでは、「キーワード」を指定して検索することもできます。
が、この「キーワード」は「イベント名」だけでなく、「イベント説明」なども検索対象になっています。

そのため「QA」「テスト」「品質」などのキーワードで検索すると、イベント説明に「QAタイム (10分間)」「資格講座受講前の基礎力確認テスト」「高品質なアプリケーション開発」などが含まれたイベントがヒットしてしまい、これまたノイズが多すぎでダメでした。

ヒットしたイベント群の中から タイトルに「QA」「テスト」「品質」が含まれるものを抽出する…というアプローチも考えたんですが、タイトルにそれらの単語を含まないテスト系イベントも多かったため断念しました。