複数のBacklog APIを取得しGoogle Apps Scriptで毎日決まった時間にSlack通知する

Posted on:2021-01-29

普段の仕事が、会社端末と仕事受注先の端末の2台で仕事をしています。
基本的に昼は受注している端末で作業などを行って且つタスク管理はJIRAがメインで使っていますが、自社などの仕事の殆どがBacklogを利用している状況な感じになります。

別端末のほうに会社で利用しているものは基本インストールなどできないため、BacklogにJIRAで今担当している仕事をBacklogに月ごとにタスクを切りそこに入力したものを手動でいれるようにしています。
状況を把握するために記載しているもののSlackで今日なにするかだけは会社のSlackに流す必要があったので、それ用の通知を出せるようにして自動化をしました。

また、受託案件が基本の会社になるのでBacklogも自社で利用しているものや案件ごとに別のBacklogなどを利用するケースが結構おきます。
そのため今自分にボールがあるタスクはどれになるのか把握するためにBacklogのAPIを利用してGoogle Apps Scriptを使い毎日決まった時刻に通知するようにしてみました。

調べてなかですでにやってる方は多いのですが、複数のプロジェクトを横断的に通知する方法はなかったので、今回はその方法を紹介したいと思います。

利用する技術

  • Backlog API
  • Google Apps Script
  • Google Calendar
  • Slack

Google Apps Script

ベースとなるスクリプトを入力するプロジェクトを作っていきます。

https://www.google.com/script/start/

Start Scripting をクリックして始めます。

01

新しいプロジェクト をクリックして、Apps Script を開きます。

02

プロジェクトを追加すると上の画面になります。編集する部分は以下になります。

  • スクリプトのファイル名
  • プロジェクト名
  • エディタ編集画面

ファイル名とプロジェクト名は管理しやすい名前を設定しておきます。ここでは以下に設定しておきます。

  • ファイル名:backlog.gs
  • プロジェクト名:自分のbacklogタスク

個人の設定からAPI Keyを登録

03

各Backlogにアクセスし個人の設定画面から API を登録していきましょう。
このとき用途のメモは残しておくとよいでしょう。登録を押すことでAPIが発行されます。

Apps Scriptに必要な値を設定

横断的にAPIの値やBacklogのスペースのURLやユーザIDなどが必要になるため、変数としてセットしておきます。

// 公式の課題一覧の取得に記載されているURL
const endpoint = "/api/v2/issues";
// 利用しているBacklogのドメイン
const baseUrls = {
  exmaple1: "https://exmaple1.backlog.com",
  exmaple2: "https://exmaple2.backlog.jp",
  exmaple3: "https://exmaple3.backlog.jp",
};
// Backlogから取得したAPIキー
const apiKey = {
  exmaple1: "XXXXXXXXXXXXXXXXXXXXXXXXXXX",
  exmaple2: "XXXXXXXXXXXXXXXXXXXXXXXXXXX",
  exmaple3: "XXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
// 各BacklogのユーザID
const assigneeId = {
  exmaple1: 1111,
  exmaple2: 2222,
  exmaple3: 3333,
};

BacklogのユーザIDは、管理画面からは参照できないため(一般ユーザ)以下の方法で取得することができます。

公式サイト

https://developer.nulab-inc.com/ja/docs/backlog/api/2/get-project-user-list/

/api/v2/projects/:projectIdOrKey/users

公式のドキュメントを参考に PawInsomnia などのGUIツールやシェルなどでレスポンスを確認してみましょう。
レスポンスを取得して、usersの一覧から自分のアカウント情報を確認して id の情報を先程のスクリプトで定義した assigneeId に記述しておきます。

[
  {
    "id": 111111,
    "userId": null,
    "name": "ユーザー名",
    "roleType": 1,
    "lang": null,
    "mailAddress": "xxxxxxxx@xxxxx.co.jp",
    "nulabAccount": null
  }
]

以下のコマンドでも取得することが可能です。レスポンスを綺麗に確認したい場合は、 jq コマンドをインストールしておきましょう。
実行結果はJSONファイルとして出力させておきます。 ${SPACE} ${projectIdOrKey} ${API_KEY} は各自の値を入力して実行します。

// jq コマンドをインストール
brew install jq
// 出力用のコマンド
curl --globoff 'https://${SPACE}.backlog.jp/api/v2/projects/${projectIdOrKey}/users?apiKey=${API_KEY}' | jq '.' > backlog.json

スプレッドシートで環境変数を管理する

環境変数をコードで管理してましたが、Backlogが追加するたびに変数を追加するのは運用的に辛いため環境変数だけはスプレッドシートで管理する方法を紹介します。
この方法で行えば、上記の変数も不要になり新しくBacklogが追加するたびにコードを編集せずに運用できます。

スプレッドシート

まずは新規でスプレッドシートを作成します。
ここでは、A列に space名、B列に baseUrl名 、C列に apiKey 、 D列に userID を用意します。
userIDは数字ですが、セル上は 書式なしテキスト にしておきます。数字の場合で 0 が入ると小数点になるため

次に Apps Script側を編集していきます。
SpreadsheetApp.openById('XXXXXXXXX') はスプレッドシートのURLにある spreadsheets/d//edit#gid=0 の間にある文字列を指します。
.getSheetByName はシート名を入力します。ここでは環境変数というシート名にしています。

ロジックは変えずに値を環境変数として利用するため、以下のような ENV_ から始まるオブジェクトの箱を用意しておきます。 sheet.getLastRow(); で最後の行番号を取得することができるため今後追加されてもコードをいじらずシートの更新だけで対応可能にしておきます。
space の列の値を取得し、 space を元に reduce をかけてそれぞれの key / value を生成させておきます。この関数がロジックで利用するための環境変数が作られます。

let ENV_BASE_URLS = {};
let ENV_API_KEY = {};
let ENV_ASSIGNEEID = {};
// 環境変数の作成
function envFunction() {
  const sheet = SpreadsheetApp.openById("XXXXXXXXX").getSheetByName("環境変数");
  const lastRow = sheet.getLastRow();
  const space = sheet.getRange(2, 1, lastRow - 1).getValues();
  const base_urls = sheet.getRange(2, 2, lastRow - 1).getValues();
  const api_key = sheet.getRange(2, 3, lastRow - 1).getValues();
  const assignee_id = sheet.getRange(2, 4, lastRow - 1).getValues();
  ENV_BASE_URLS = space.reduce(
    (acc, cur, index) => ({ ...acc, [cur[0]]: base_urls[index][0] }),
    {}
  );
  ENV_API_KEY = space.reduce(
    (acc, cur, index) => ({ ...acc, [cur[0]]: api_key[index][0] }),
    {}
  );
  ENV_ASSIGNEEID = space.reduce(
    (acc, cur, index) => ({ ...acc, [cur[0]]: assignee_id[index][0] }),
    {}
  );
}

Slackへの送信関数を実装

Slackへの送信関数を作る前にWebhookのURLを取得しておきます。以前の設定では Incoming WebHooks を利用することが出来ていましたが、新しい Incoming Webhook からは個別アプリから登録に変更されました。自社では旧方式があったためWebhookのURLは旧方式で設定しています。

slackのIncoming webhookが新しくなっていたのでまとめてみた こちらの記事が参考になりました。
仕様は変わりましたが、今回はPOSTするだけの単純なものになるので、新しいアプリで設定した方も同様にWebhookのURLを取得して、 postUrl に設定しておきます。

payload の部分に message が入るように関数を作ります。
UrlFetchApp.fetch はGoogle Apps Scriptが提供するメソッドになります。このメソッドを使うことでHTTPリクエストを送ることができます。
Logger.log も同様に実行ログを確認するメソッドになります。実行ログパネルを開くことで内部の値を確認することが可能です。

// Slackに送信
function sendSlack(message) {
  // Webhook URL
  const postUrl = 'https://hooks.slack.com/services/XXXXXXXXX;
  const json = {
    'text': message
  };
  const payload = JSON.stringify(json);
  const options = {
    'method': 'post',
    'contentType': 'application/json',
    'payload': payload
  };
  Logger.log(payload)
  UrlFetchApp.fetch(postUrl, options);
}

タスクを取得しSlackへPOSTする関数の実装

タスクを取得して先程の作成したSlackへの送信関数へ受け渡す処理を記述します。
先程作成した環境変数用の envFunction を読み込んでおきます。
メッセージの雛形は、ID・課題・リンクで出力するようにします。

変数 baseUrls を元にループします。baseUrlsのkeyは、API_KEYやユーザIDともに同じKeyにしておきました。ループ中にkey名を利用してエンドポイントを作成するように実装しています。
環境変数で定義した ENV_BASE_URLS を元にループさせて、エンドポイントを作成するように実装しています。
最後に取得した message をslackの送信関数に渡します。

function myTasks() {
  envFunction(); // ここで環境変数を読み込む
  for (let space in ENV_BASE_URLS) {
    const endpointUrl =
      ENV_BASE_URLS[space] +
      endpoint +
      "?apiKey=" +
      ENV_API_KEY[space] +
      "&assigneeId[]=" +
      ENV_ASSIGNEEID[space] +
      "&statusId[]=" +
      2;
    const response = UrlFetchApp.fetch(endpointUrl);
    const json = JSON.parse(response.getContentText());
    let message = "";
    for (let i in json) {
      // 課題の数だけループを回す
      const issueKey = json[i]["issueKey"]; // 課題のID
      const summary = json[i]["summary"];
      const link = ENV_BASE_URLS[space] + "/view/" + json[i].issueKey;
      message += `${issueKey}:${summary} ${link}`;
      if (json[i] != json[json.length - 1]) {
        message += "\n";
      }
    }
    message && sendSlack(message);
  }
}

Google Calendarに営業日・休日祝日・有給に合わせたトリガー作成

Googleカレンダーと連携してトリガーを作っていきます。
実装としては、休日祝日・有給・会社休業日を真偽値で返す関数を作っていきます。

休日祝日の判定関数

休日祝日の判定関数を作ります。
土日の判定は、 .getDay() メソッドを使うことで判定ができます。

Date.prototype.getDay()

.getDay() メソッドを使うと、返り値は0〜6の値を受け取ることができます。

  • 日曜日は「0」
  • 土曜日は「6」

0と6だった場合は return false を返すように条件分岐させておきます。
次に祝日の判定については、 CalendarApp.getCalendarById を利用して日本の祝日情報を取得します。

カレンダーIDはGoogle Calendar 左カラムの 日本の祝日 設定からカレンダーIDを取得することができます。 .getEventsForDay(date) で祝日のイベント情報を取得することができます。祝日があった場合は、[CalendarEvent] という配列でなければ空の配列を取得することができます。
.length を使って、土日祝日以外は true を返すようにしています。

// 休日祝日の判定(営業日)
function isBusinessDay(date) {
  if (date.getDay() == 0 || date.getDay() == 6) {
    return false;
  }
  const calJa = CalendarApp.getCalendarById(
    "ja.japanese#holiday@group.v.calendar.google.com"
  );
  if (calJa.getEventsForDay(date).length > 0) {
    return false;
  }
  return true;
}

有給・会社休業日の判定関数

会社の休業日だったり、自分の有給は getTitle メソッドを使ってイベント名を取得します。 some() を使って、特定のタイトルがある場合は true を返します。
true を返しておくと次のトリガー関数を実装する際に利用します。

// 有給の判定
function isMyBusinessDay(date) {
  const calendar = CalendarApp.getCalendarById(
    "XXXXX@group.calendar.google.com"
  );
  const events = calendar.getEventsForDay(date);
  const check = events.some(event => event.getTitle() === "有給");
  return check;
}
// 会社休業日の判定
function isCompanyDay(date) {
  const calendar = CalendarApp.getCalendarById(
    "XXXXX@group.calendar.google.com"
  );
  const events = calendar.getEventsForDay(date);
  const check = events.some(
    event =>
      event.getTitle() === "年末年始休業" || event.getTitle() === "夏休み"
  );
  return check;
}

毎日決まった時間に実行する関数の実装

最後に毎日決まった時間に実行する関数を作っていきます。
setHourssetMinutes を利用して、9時40分に実行するようにしています。決まった時間は各自で設定しておきます。

先程作成した、 isCompanyDayisMyBusinessDay 関数に指定した時間を渡して true だった場合は休日判定になるため実行を止めます。

isBusinessDaytrue だった場合は、営業日のため ScriptApp.newTrigger のトリガーで myTask を実行します。

function setTrigger() {
  const time = new Date();
  time.setHours(9);
  time.setMinutes(40);
  //
  if (isCompanyDay(time) || isMyBusinessDay(time)) return false;
  if (isBusinessDay(time))
    ScriptApp.newTrigger("myTasks").timeBased().at(time).create();
}

始業が10時になるので、その前にタスクを出すようにするため 9時40分 に実行するようにセットしました。

トリガーの設定

最後にApps Script側にトリガーの設定を行って完成です。
実行する関数と時間主導型・日付ベースタイマーを設定し時間は実行する時間帯を選択しておきます。

トリガーの設定

完成コード

最終的なコードは以下のようになりました。適材適所は各自の情報を入力すると使えると思います。
ちょっとした業務の自動化みたいなTipsでした。

let ENV_BASE_URLS = {};
let ENV_API_KEY = {};
let ENV_ASSIGNEEID = {};
// 環境変数の作成
function envFunction() {
  const sheet = SpreadsheetApp.openById("XXXXXXXXX").getSheetByName("環境変数");
  const lastRow = sheet.getLastRow();
  const space = sheet.getRange(2, 1, lastRow - 1).getValues();
  const base_urls = sheet.getRange(2, 2, lastRow - 1).getValues();
  const api_key = sheet.getRange(2, 3, lastRow - 1).getValues();
  const assignee_id = sheet.getRange(2, 4, lastRow - 1).getValues();
  ENV_BASE_URLS = space.reduce(
    (acc, cur, index) => ({ ...acc, [cur[0]]: base_urls[index][0] }),
    {}
  );
  ENV_API_KEY = space.reduce(
    (acc, cur, index) => ({ ...acc, [cur[0]]: api_key[index][0] }),
    {}
  );
  ENV_ASSIGNEEID = space.reduce(
    (acc, cur, index) => ({ ...acc, [cur[0]]: assignee_id[index][0] }),
    {}
  );
}
// Slackに送信
function sendSlack(message) {
  // Webhook URL
  const postUrl = "https://hooks.slack.com/services/XXXXXXXXXXXXXXXXXXXX";
  const json = {
    text: message,
  };
  const payload = JSON.stringify(json);
  const options = {
    method: "post",
    contentType: "application/json",
    payload: payload,
  };
  // Logger.log(payload)
  UrlFetchApp.fetch(postUrl, options);
}
// MyTasks
function myTasks() {
  envFunction(); // ここで環境変数を読み込む
  for (let space in ENV_BASE_URLS) {
    const endpointUrl =
      ENV_BASE_URLS[space] +
      endpoint +
      "?apiKey=" +
      ENV_API_KEY[space] +
      "&assigneeId[]=" +
      ENV_ASSIGNEEID[space] +
      "&statusId[]=" +
      2;
    const response = UrlFetchApp.fetch(endpointUrl);
    const json = JSON.parse(response.getContentText());
    let message = "";
    for (let i in json) {
      // 課題の数だけループを回す
      const issueKey = json[i]["issueKey"]; // 課題のID
      const summary = json[i]["summary"];
      const link = ENV_BASE_URLS[space] + "/view/" + json[i].issueKey;
      message += `${issueKey}:${summary} ${link}`;
      if (json[i] != json[json.length - 1]) {
        message += "\n";
      }
    }
    // 0件以外をslackに投稿する
    message.length !== 0.0 && sendSlack(message);
  }
}
// 有給の判定
function isMyBusinessDay(date) {
  const calendar = CalendarApp.getCalendarById(
    "XXXXX@group.calendar.google.com"
  );
  const events = calendar.getEventsForDay(date);
  const check = events.some(event => event.getTitle() === "有給");
  return check;
}
// 会社休業日の判定
function isCompanyDay(date) {
  const calendar = CalendarApp.getCalendarById(
    "XXXXX@group.calendar.google.com"
  );
  const events = calendar.getEventsForDay(date);
  const check = events.some(
    event =>
      event.getTitle() === "年末年始休業" || event.getTitle() === "夏休み"
  );
  return check;
}
function setTrigger() {
  const time = new Date();
  time.setHours(9);
  time.setMinutes(40);
  if (isMyBusinessDay(time)) return false;
  if (isBusinessDay(time))
    ScriptApp.newTrigger("myTasks").timeBased().at(time).create();
}

Tipsメモ

Slackでメンションをつけたい場合は、 UserName ではだめでした。メンションを飛ばす場合は <@userID> を設定するようでした。

メンションをつける際はプロフィール画面で参照する

参考サイト