Google Apps ScriptでBacklog APIを操作してみた【GAS】

業務でプロジェクト管理ツールとして、Backlogを利用することになった為学習中ですが、せっかくのウェブサービスながらも、十分に活用されていない側面があったりします。やはり多機能なのは良いのですが、それが故に第一印象で億劫になりがちなのがウェブサービスです。

そこで、これをGoogle Apps Scriptから叩いて使えるようにしてみようと、自宅でも30日間無償のプランを申し込んで、Backlog APIを使ってみようと思い記事にしています。

今回利用するスプレッドシート等

OAuth2.0認証を行う仕組みとなっています。API Keyでの利用もできますがセキュアにするためにはOAuth2.0認証を使いましょう。また、認証用のライブラリとしてOAuth2 library for Google Apps Scriptを利用しています。

実際の現場では、例えばGMailアドオンとして構築して、Gmail→Backlogに課題作成や登録といった機能を自分は装備しています。

Google Apps Scriptでちょっとした顧客管理台帳を作る - GMailアドオン編【GAS】

Google Spreadsheet用の組織内アドオンを作成する【GAS】

Backlog APIの制限

2021年7月からBacklogにも他のウェブサービス同様のリクエスト制限が設けられています。上限値を超えると429エラーが返ってくるので、その間はリクエストを投げることができなくなります。制限はユーザ単位でありAPIキー単位ではありません。また、API毎に制限が設けられていて一律ではないので要注意です。詳細はレート制限を御覧ください。

概ね以下の4種類に分類されます。

APIの種類 有料プラン フリープラン
読み込み 600 回/分 以内 60 回/分 以内
更新 150 回/分 以内 15 回/分 以内
検索 150 回/分 以内 15 回/分 以内
アイコン取得 60 回/分 以内 6 回/分 以内

この制限に掛からないように、1分の間で上限を超えないようにウェイトを入れるなどの処理が必要になります。また、読み込み等では1度に取得できる件数が最大100件となっているので、例えば課題全部を取りたいとなった場合、

  • 課題数の取得APIにて件数の総数を取得する
  • 課題数の総数 / 100でループの回数を計算する
  • 課題取得のリクエストでcount=100を指定する
  • 課題取得のリクエストでoffsetにて何件目からのデータの取得をするかを指定する(101と指定すると101件目からの取得になる)

といった工夫が必要になります。

事前準備

GAS側の準備

ライブラリの追加

以下の手順でOAuth2 library for Google Apps Scriptライブラリを追加しましょう。

  1. スクリプトエディタを開きます。
  2. サイドバーよりより「ライブラリ」の+ボタンをクリック
  3. ライブラリを追加欄に「1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF」を追加します。
  4. 今回はバージョンは43を選択してみます。
  5. 保存ボタンを押して完了

これで、OAuth2.0認証にまつわる様々な関数を手軽に利用できるようになります。

図:ライブラリを追加した様子

コールバックURLを取得する

コールバックURLとは、認証を完了しAccess Tokenを取得したら戻るべきURLを指定するものです。これは、スクリプトIDをもとに作られているので、スクリプトIDを取得して組み立てます。

  1. スクリプトエディタのサイドバーより「プロジェクトの設定」を開く
  2. 情報の中にある「スクリプトID」を控えておく。
  3. https://script.google.com/macros/d/スクリプトID/usercallback として組み立てる。これがコールバックURLとなる。

図:スクリプトIDはファイル毎に異なるのです。

Backlog側の準備

Backlog APIは実はBacklogのアカウントではなく、別にヌーラボのアカウントを作成してそちらでログインしてアプリを作成し、Client IDとClient Secretを取得する仕組みになっています。

アプリの登録

以下の手順でアプリを登録し、Client IDとClient Secretを取得します。

  1. Backlog Developerにアクセスする
  2. アプリケーション管理ページをクリックする
  3. NuLabにログインする
  4. 新規登録をする
  5. Redirect URLには前述のコールバックURLを入れる
  6. アプリのアイコン等は適当にpng画像をアップロードしておく。
  7. アプリ名と説明、そして自身のウェブサイト等を入れて、作成をクリック
  8. すると、Client IDおよびClient Secretが入手できるので控えておく。

図:アプリ登録中の様子

スペース名とプロジェクトIDを取得する

アプリでは認証URLやToken URLにおいて、自身が作成したBacklogのページのURLに含まれてるスペース名とProjectIDが必要になります。スペース名とは、メインのURL(例:https://hogehoge.backlog.com)にあるhogehogeの部分がスペース名となります。また、プロジェクトに入ると、URLにprojectIdの値が入ってるので、これも控えておきます。(例:https://hogehoge.backlog.com/find/プロジェクト名?projectId=362158

取得したスペース名で以下のようにAuth URLやToken URLを構築します。

var spacename = "ここにスペース名を入れる";
var authurl = "https://" + spacename + ".backlog.com/OAuth2AccessRequest.action?"
var tokenurl = "https://" + spacename + ".backlog.com/api/v2/oauth2/token"

backlog.comの部分は、お使いの環境によっては、backlog.jpの場合もあるので注意してください。

認証フローを作成する

GAS側コード

//認証用の各種変数
var appid = 'ここにクライアントIDを入れる';
var appsecret='ここにクライアントシークレットを入れる';
var projectId = "ここに操作するプロジェクトIDを入れる";

function startoauth(){
  //UIを取得する
  var ui = SpreadsheetApp.getUi();
  
  //認証済みかチェックする
  var service = checkOAuth();
  if (!service.hasAccess()) {
    //認証画面を出力
    var output = HtmlService.createHtmlOutputFromFile('template').setHeight(450).setWidth(500).setSandboxMode(HtmlService.SandboxMode.IFRAME);
    ui.showModalDialog(output, 'OAuth2.0認証');
  } else {
    //認証済みなので終了する
    ui.alert("すでに認証済みです。");
  }
}

//アクセストークンURLを含んだHTMLを返す関数
function authpage(){
  var service = checkOAuth();
  var authorizationUrl = service.getAuthorizationUrl();
  var html = "<center><b><a href='" + authorizationUrl + "' target='_blank' onclick='closeMe();'>アクセス承認</a></b></center>"
  return html;
}

//認証チェック
function checkOAuth() {
  return OAuth2.createService("backlog")
    .setAuthorizationBaseUrl(authurl)
    .setTokenUrl(tokenurl)
    .setClientId (appid)
    .setClientSecret(appsecret)
    .setCallbackFunction ('authCallback')
    .setPropertyStore (PropertiesService.getUserProperties ());
}

//認証コールバック
function authCallback(request) {
  var service = checkOAuth();
  var isAuthorized = service.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput("認証に成功しました。ページを閉じてください。");
  } else {
    return HtmlService.createHtmlOutput("認証に失敗しました。");
  }
}

//ログアウト
function reset() {
  checkOAuth().reset();
  SpreadsheetApp.getUi().alert("ログアウトしました。")
}
  • Client IDおよびClient Secretを記述します。
  • 操作対象のプロジェクトIDを記述します。
  • startoauthでOAuth2.0認証が実行されてダイアログが出現します。
  • 認証がされるとユーザプロパティに認証結果のTokenなどが保存されます。
  • API Keyによる認証もありますが、企業ユースではよりセキュアなOAuth2.0認証にすべきでしょう。

HTML側コード

<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<script type="text/javascript" src="https://apis.google.com/js/api.js"></script>
<script>
  google.script.run.withSuccessHandler(onSuccess).authpage();
  
  function onSuccess(data){
    document.getElementById("kinoko").innerHTML = data;
  }

</script>

<style type="text/css">
  /* --- ボックス --- */
  div.section {
    width: 480px; /* ボックスの幅 */
    background-color: #ffffff; /* ボックスの背景色 */
    border: 1px #c0c0c0 solid; /* ボックスの境界線 */
    font-size: 100%; /* ボックスの文字サイズ */
  }
  
  /* --- 見出し --- */
  div.section h3 {
    margin: 0; /* 見出しのマージン */
    padding: 6px 10px; /* 見出しのパディング(上下、左右) */
    background-color: #f5f5f5; /* 見出しの背景色 */
    border-bottom: 1px #c0c0c0 solid; /* 見出しの下境界線 */
    font-size: 120%; /* 見出しの文字サイズ */
  }
  
  /* --- ボックス内の段落 --- */
  div.section p {
    margin: 1em 10px; /* 段落のマージン(上下、左右) */
  }
</style>

<div class='section' style="text-align: center">
  <img border="0" src="https://officeforest.org/library/oauth2.png" alt="oauth2">
  <h3 id='header'>OAuth認証の許可が必要です。</h3>
  <hr>
</div>
<div id="info">
<p>
このスクリプトは、Backlog APIにアクセスするために、特別なログイン処理を利用しています。<br>
既に特別なログインに関する設定はなされており、承認がされるとプログラムを実行することが出来ます。この承認がなされない場合、プログラムの実行に制限が掛かり、
処理が続行できません。<br><br>

<div id="kinoko" style="text-align: center">
   <img border="0" src="https://officeforest.org/wp/library/ProgressSpinner.gif" width="30" height="30">
</div>
</div>
<p>
<script>
function closeMe(){
  if(google && google.script && google.script.host){
      google.script.host.close();
  } else if(window && window.close){
      window.close();
  } 
}
</script>
</div>
  • 認証実行時に出てくるダイアログ用のファイル(template.html)の中身です。

認証を実行する

startauth関数を実行するとダイアログが出てきて、アクセス認証のリンクが出てきます。これをクリックすると、Backlog側での許可画面が出るので、許可を実行すると、Access Token等が取得されてユーザプロパティに保存されます。

Refresh Tokenの処理はライブラリが行ってくれてるので、特に意識する必要はありません。

図:GAS側で出てくるアクセス認証画面

図:許可をするとAccess Tokenが手に入る

ソースコード

課題の件数を取得する

プロジェクト内の課題をすべて取得しようとなると、1度に取得できる件数は100件までです。その後はoffset値を利用して次の100件を取得するといったことが必要になります。offsetは0から始まるので、200〜の場合はoffsetは200を指定すると言ったことが必要。

そこで総件数/100でループの回数を出して、offsetで指定して次の開始レコード位置から課題の取得を回すことになります。レート制限に掛からないようにウェイトを入れておく必要もあります。

//課題の全件数を取得して返す
function getkadaicount(){
  let ui = SpreadsheetApp.getUi();

  //トークン確認
  var service = checkOAuth();

  if (service.hasAccess()) {   
    //エンドポイントを構築
    var endpoint = "https://" + spacename + ".backlog.com/api/v2/issues/count?";

    //リクエストオプションをつける
    var request = endpoint + "projectId[]=" + projectId;

    //リクエストヘッダ
    let header = {
      Authorization: 'Bearer ' + service.getAccessToken()
    }

    //リクエストオプション
    let options = {
      method: "GET",
      headers: header,
      muteHttpExceptions: true
    }

    //リクエスト実行
    const response = UrlFetchApp.fetch(request, options);

    //リクエスト結果を取得する
    const result = JSON.parse(response.getContentText());

    //リクエスト結果を表示
    countman = result.count

    //件数を返す
    return countman;

  }else{
    ui.alert("認証が実行されていませんよ。");
  }
}

課題一覧データを一括で全部取得する

現在登録されている課題の一覧を取得して、スプレッドシートに書き出します。この書き出しですが、プレミアムプラン以上の場合カスタムフィールドを追加する事ができるため、一覧からカスタムフィールドの値をとってくる事も可能です(プランの中に書かれてないですけれどね・・・)。課題の一覧を取得するAPIはこちらのエントリーです。

//backlogデータの課題一覧を取得して出力する(完了したもの以外を取得する)
function getbacklog() {
  let ui = SpreadsheetApp.getUi();

  //課題の総件数を取得する
  let totalman = getkadaicount();

  //一度に取得する件数を指定
  let count = 3;

  //ループする回数を指定
  let loopman = Math.ceil(totalman / count);

  //トークン確認
  let service = checkOAuth();

  if (service.hasAccess()) {   
    //エンドポイントを構築
    let endpoint = "https://" + spacename + ".backlog.com/api/v2/issues?";

    //リクエストヘッダ
    let header = {
      Authorization: 'Bearer ' + service.getAccessToken()
    }

    //リクエストオプションをつける(1度に最大100件取得可能)
    //statusIdは複数つけられるけれど、1個ずつ指定する必要がある(4が完了)
    let request = endpoint + ""
                  + "&projectId[]=" + projectId
                  + "&statusId[]=" + 1
                  + "&statusId[]=" + 2
                  + "&statusId[]=" + 3
                  + "&count=" + count;

    //送信オプション
    let options = {
      method: "GET",
      headers: header,
      muteHttpExceptions: true
    }

    //offset値を指定
    let offset = 0;

    //書き込み用配列を用意
    let array = [];

    for(let i = 0;i<loopman;i++){
      //offset値を指定する
      let sendman = request + "&offset=" + offset

      //リクエスト実行
      const response = UrlFetchApp.fetch(sendman, options);

      //リクエスト結果を取得する
      const result = JSON.parse(response.getContentText());

      //レスポンスからデータを構築する
      let rectotal = result.length;

      for(let j = 0;j<rectotal;j++){
        //resultデータから書き込み用レコードを構築する
        let temparr = []

        //1つ目のレコードを取り出す
        let rec = result[j];

        //temparrにpushしていく
        temparr.push(rec.id);
        temparr.push(rec.summary);
        temparr.push(rec.priority.name);
        temparr.push(rec.status.name)
        temparr.push(rec.created)
        temparr.push(rec.startDate)
        temparr.push(rec.dueDate)

        //temparrをarrayに追加する
        array.push(temparr);
      }

      //offset値を加算する(件数分足す)
      offset = offset + count;

      //ウェイトを入れる(1秒間)
      Utilities.sleep(1000)

    }

    //配列データをidを基準に昇順ソート
    array.sort(function(a,b){return(a[0] - b[0]);});

    //指定範囲内をクリア
    let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("backlog")
    sheet.getRange("A2:G").clearContent();

    //スプレッドシートに書き出しする
    let lastColumn = array[0].length;  //カラムの数を取得する
    let lastRow = array.length;      //行の数を取得する
    sheet.getRange(2,1,lastRow,lastColumn).setValues(array); 

    //終了メッセージ
    ui.alert("全件データを取得しました。")

  }else{
    ui.alert("認証が実行されていませんよ。");
  }
}
  • 今回はテストで1度に3件ずつ取得します(故にcountを3で指定。通常はここを100を指定する)
  • loopmanにてループさせる回数を指定(小数点以下切り上げで回数を算出する)
  • offsetは1回目は0から。次のoffset値は3からとなるので、offset値にcountの数を加算して次の開始位置を決める
  • 今回は必要最低限のフィールドの値のみを指定。GETリクエストなのでURLにオプションを加える
  • 最終的に書き込み用配列にすべての値を含めて、一括書き出しを行う。
  • レート制限回避で軽く1000msのウェイトをUtilities.sleepで入れてあります。

図:課題リストの一覧

図:APIで取得した結果

課題を登録する

課題の登録を行うには、プロジェクトIDの他にタイトル、優先度課題のタイプの3種類が最低限必要です。優先度と課題のタイプはIDで指定する必要があるのですが、ドキュメントでは数値となってるものの、リクエストをする場合はこの数値は文字列で指定しないとエラーになるので嵌りどころです。

優先度は、2,3,4とありそれぞれ高・中・低となっていますが、課題のタイプはプロジェクト毎に値が異なるので事前に取得して例えば、タスクの課題タイプのIDがいくつなのか?を知っておかないと指定が出来ません。

const statusCode = response.getResponseCode();を使ってレスポンスのステータスコードで処理を分岐するようにするとなお良いでしょう。
//課題を投稿する
function addKadai(){
  let ui = SpreadsheetApp.getUi();

  //トークン確認
  var service = checkOAuth();

  if (service.hasAccess()) {  
    //エンドポイントを構築
    var endpoint = "https://" + spacename + ".backlog.com/api/v2/issues";

    //リクエストヘッダ
    let header = {
      Authorization: 'Bearer ' + service.getAccessToken(),
      "Content-Type": "application/x-www-form-urlencoded"
    }

    //リクエストボディ
    let payload = {
      "projectId": projectId,
      "summary": "乗鞍白骨温泉で温泉に浸かる",
      "issueTypeId": "1823228",   //ここは予め知っておく必要がある
      "priorityId": "3"  //今回は中で決め打ち
    }

    //リクエストオプション
    let options = {
      method: "POST",
      headers: header,
      muteHttpExceptions: true,
      payload:payload
    }

    var response = UrlFetchApp.fetch(endpoint, options);

    //リクエスト結果を取得する
    const result = JSON.parse(response.getContentText());

    console.log(JSON.stringify(result))

    //終了メッセージ
    ui.alert("課題の登録が完了しました。データの一括取得を再実行してください。")
  
  }else{
    ui.alert("認証が実行されていませんよ。");
  }
}

//課題種別IDの一覧を取得する
function getPriorityId(){
  let ui = SpreadsheetApp.getUi();

  //トークン確認
  var service = checkOAuth();

  if (service.hasAccess()) {  
    //エンドポイントを構築
    var endpoint = "https://" + spacename + ".backlog.com/api/v2/priorities";

    //リクエストヘッダ
    let header = {
      Authorization: 'Bearer ' + service.getAccessToken()
    }

    //リクエストオプション
    let options = {
      method: "GET",
      headers: header,
      muteHttpExceptions: true
    }

    var response = UrlFetchApp.fetch(endpoint, options);

    //リクエスト結果を取得する
    const result = JSON.parse(response.getContentText());

    console.log(result)

    //終了メッセージ
    ui.alert("優先度を取得しました。")

  }else{
    ui.alert("認証が実行されていませんよ。");
  }
}

//課題種別IDの一覧を取得する
function getIssueTypeId(){
  let ui = SpreadsheetApp.getUi();

  //トークン確認
  var service = checkOAuth();

  if (service.hasAccess()) {  
    //エンドポイントを構築
    var endpoint = "https://" + spacename + ".backlog.com/api/v2/projects/" + projectId + "/issueTypes";

    //リクエストヘッダ
    let header = {
      Authorization: 'Bearer ' + service.getAccessToken()
    }

    //リクエストオプション
    let options = {
      method: "GET",
      headers: header,
      muteHttpExceptions: true
    }

    var response = UrlFetchApp.fetch(endpoint, options);

    //リクエスト結果を取得する
    const result = JSON.parse(response.getContentText());

    console.log(result)

    //課題タイプを書き出す
    let array = [];
    for(var i = 0;i<result.length;i++){
      //レコードを1個取り出す
      let rec = result[i];

      //一時配列を構築する
      let temparr = [
        rec.id,
        rec.name
      ]

      //arrayに追加
      array.push(temparr)
    }

    //指定範囲内をクリア
    let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("issuetype")
    sheet.getRange("A2:B").clearContent();

    //スプレッドシートに書き出しする
    let lastColumn = array[0].length;  //カラムの数を取得する
    let lastRow = array.length;      //行の数を取得する
    sheet.getRange(2,1,lastRow,lastColumn).setValues(array); 

    //終了メッセージ
    ui.alert("課題タイプを取得しました。")

  }else{
    ui.alert("認証が実行されていませんよ。");
  }
}

図:無事に課題が登録されました。

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)