Gmail Add-onからBacklogのステータスを更新する

この記事は ハンズラボAdvent Calendar 2018 24日目の記事です。

# 背景

ハンズラボの内製チームエンジニアの @ansark88 です。

私の所属しているチームでは、日々全国の東急ハンズの社員から問い合わせがあるため、日直を決めて対応しています。

専用のメールアドレスに問い合わせが送られてくると、まずBacklogにて課題が作成されます。
そして問い合わせに対応したら、Backlogのステータスを更新してクローズにするのですが、どうしてもBacklog側は放置されがちになっていました。

これをGmail Add-onを活用して、BacklogのAPIを叩き、簡単に更新されるようにします。

# 機能概要

1. 問い合わせの中から回答済みしたメールを見つけたら、アドオンから更新ボタンを押す
2. アドオンはBacklogの課題一覧を取得し、該当の課題を見つけ出す
3. アドオンは課題のコメント欄に回答メールの内容を追記し、ステータスを完了済みに変更させる

# やったこと

## GASをGit管理するため、claspをインストール

Gmail Add-onはGASで作ります。そこでGASをgit管理をできるようにGoogle公式の[clasp](https://github.com/google/clasp) を使います。

```
# claspをインストール
$ sudo npm i @google/clasp -g
```

次に https://script.google.com/home/usersettings から Apps Script API を有効にし、 claspにてログインとプロジェクト作成を行います。

```
# claspからGoogleアカウントにログイン
$ clasp login

# プロジェクトを作成(Standaloneとして作成します)
$ clasp create updateBacklog
```

## claspの動作確認

`hello.js`を作り、claspからpushして開きます。

```javascript:hello.js
function myFunction() {
    Logger.log('hello  world!');
}
```

```
$ clasp push
$ clasp open
``` 

ちゃんと反映されていました。成功です。(拡張子が`*.js`から`*.gs`に置き換わっています)

<img width="541" alt="スクリーンショット 2018-12-22 18.40.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/221596/159b9b98-d31f-355a-55f0-fd2b3addc7d7.png">

なおスクリプトエディタ側の更新を反映させたいときは `clasp pull`でローカルに持ってことができます。

## メールの内容を抜き出す

[Quickstart: Gmail Add-on](https://developers.google.com/gmail/add-ons/guides/quickstart) を参考にして、まずマニュフェストファイルを書き換えます。
(これはスクリプトエディタの[表示] → ｢マニュフェストファイルを表示｣から開けます)

```json:appsscript.json
{
  "oauthScopes": [
    "https://www.googleapis.com/auth/gmail.addons.execute",
    "https://www.googleapis.com/auth/gmail.addons.current.message.metadata",
    "https://www.googleapis.com/auth/gmail.modify",
    "https://www.googleapis.com/auth/gmail.addons.current.message.readonly" 
  ],
  "gmail": {
    "name": "Gmail Add-on Quickstart - QuickLabels",
    "logoUrl": "https://www.gstatic.com/images/icons/material/system/1x/label_googblue_24dp.png",
    "contextualTriggers": [{
      "unconditional": {
      },
      "onTriggerFunction": "buildAddOn"
    }],
    "openLinkUrlPrefixes": [
      "https://mail.google.com/"
    ],
    "primaryColor": "#4285F4",
    "secondaryColor": "#4285F4"
  }
}
```

はじめにメールのスレッドの情報を抜き出すアドオンを作るところから始めてみます。
これも[Quickstart: Gmail Add-on](https://developers.google.com/gmail/add-ons/guides/quickstart) のソースをベースに、スレッドの先頭件名とスレッドの選択位置の送信者と本文を表示にさせるように編集。

```javascript:updateBacklog.js
function buildAddOn(e) {
  // Activate temporary Gmail add-on scopes.
  var accessToken = e.messageMetadata.accessToken;
  GmailApp.setCurrentMessageAccessToken(accessToken);

  var messageId = e.messageMetadata.messageId;
  var message = GmailApp.getMessageById(messageId);
  
  var thread = message.getThread();
  var messages = thread.getMessages();
 
  var section = CardService.newCardSection()
    .setHeader("<font color=\"#1257e0\"><b>現在のメール情報</b></font>");       

  var subject = thread.getFirstMessageSubject();   // 件名(スレッドの最初)
  var from = message.getFrom();                // 送信元(スレッド選択位置)
  var plainbody = message.getPlainBody();      // 本文(スレッド選択位置)    

  splitbody = plainbody.split(/2...年.+?>:/);  //返信時に自動的に入る引用部分を自動挿入の文字列を利用して除外する
  body = splitbody[0];
  
  var subject_widget = CardService.newKeyValue()
                        .setTopLabel("スレッドタイトル")
                        .setContent(subject);
  
  var from_widget = CardService.newKeyValue()
                        .setTopLabel("送信者")
                        .setContent(from);             
  
  var body_widget = CardService.newKeyValue()
                        .setTopLabel("送信本文")
                        .setContent(body);

  // セクションに追加することでウィジェットを有効にする
  section.addWidget(subject_widget);
  section.addWidget(from_widget);
  section.addWidget(body_widget);

  // Build the main card after adding the section.
  var card = CardService.newCardBuilder()
    .setHeader(CardService.newCardHeader()
    .setTitle('Backlogステータス更新')
    .setImageUrl('https://www.gstatic.com/images/icons/material/system/1x/label_googblue_48dp.png'))
    .addSection(section)
    .build();

  return [card];
} 
```

そして [Google Apps Script で Gmail Add-on を作ってみよう](https://qiita.com/wezardnet/items/9d19ab6dcac7ccae00db) を参考にアドオンをgmailに追加し、サイドバーから開くと無事表示されました。(最終送信者、最終送信本文となっているのは、開発時の文面です、すみません)

<img width="346" alt="スクリーンショット 2018-12-23 22.12.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/221596/61e3eeb9-474f-a71d-d50a-da848c7fb127.png">


## Backlogとやり取りをする

### アドオンにボタンを追加する

Backlogのステータスを更新するためのテキストボタンを作成します。このボタンはクリックされると関数 `accessBacklog` を呼び出します。

```javascript
  var button_widget = CardService.newTextButton()
  .setText('Backlog更新')
  .setOnClickAction(CardService.newAction()
                    .setFunctionName('accessBacklog')
                    .setParameters({'subject': subject})
                    .setParameters({'body': body})
                    .setParameters({'from': from}));
  
  // セクションに追加することでウィジェットを有効にする
  section.addWidget(subject_widget);
  section.addWidget(from_widget);
  section.addWidget(body_widget);
  section.addWidget(button_widget);
```

### メールタイトルから対象の課題を取得する

`accessBacklog` の中身を実装していきます。BacklogのAPIキーを発行したら、GASのプロパティに書き込んでソースから分離しておきましょう。(ついでにBacklogスペースのURLも外に出しました)。

まず外部のAPIにアクセスするのでマニュフェストを更新します。

```json:appsscript.json
  "oauthScopes": [
    "https://www.googleapis.com/auth/gmail.addons.execute",
    "https://www.googleapis.com/auth/gmail.addons.current.message.metadata",
    "https://www.googleapis.com/auth/gmail.modify",
    "https://www.googleapis.com/auth/gmail.addons.current.message.readonly",
    "https://www.googleapis.com/auth/script.external_request"
  ],
```

そして取得したメールタイトルに一致する課題IDをBacklogAPIを使って取得します。

```javascript
spaceurl = PropertiesService.getScriptProperties().getProperty('spaceurl');
apikey = PropertiesService.getScriptProperties().getProperty('apikey');

//BacklogAPIにアクセス
function accessBacklog(e){
  var subject = e.parameters['subject'];
  var body = e.parameters['body'];
  var from = e.parameters['from'];
  
  var id = getIssueId(subject);
}

// メールタイトルに一致する課題IDを取得
// 使用API https://developer.nulab-inc.com/ja/docs/backlog/api/2/get-issue-list/
function getIssueId(subject){
  var url = "https://" + spaceurl + "/api/v2/issues?apiKey=" + apikey + "&projectId[]=2200&keyword=" + encodeURIComponent(subject);
 
  var res= JSON.parse(UrlFetchApp.fetch(url));
  return (res.length ? res[0].id : null);
}
```

### 課題を更新する

課題が未完了であるかチェックしてから、課題のコメント追加とステータスを｢完了｣にする処理を実装します。

`accessBacklog` に実行結果をメッセージにして返す記述を追加しました。 `CardService.newNotification` はメッセージをGmailの下側に表示させます。

```javascript
// BacklogAPIにアクセス
// コールバックとして ActionResponseを返す
function accessBacklog(e){
  var subject = e.parameters['subject'];
  var body = e.parameters['body'];
  var from = e.parameters['from'];
  var msg;
  
  var id = getIssueId(subject);
  
  if(id){
    if ( isCompleteStatus(id) ) {
      msg =  "既に課題がクローズしています";
    } else {
      var result = addComment(id,from,body);  //コメントを追加
      
      if( result ){
        var result2 = completeStatus(id); //ステータス更新
        
        if( result2 ) {
          msg = "課題をクローズしました";  
        } else {
          msg = "ステータスの更新に失敗しました";    
        }
      } else {
        msg = "コメントの追加に失敗しました";
      }
    }
  } else {
    msg = "該当する課題がありません"; 
  }
  
  // gmailの下部に通知を出す
  return CardService.newActionResponseBuilder()
    .setNotification(CardService.newNotification()
                   .setType(CardService.NotificationType.INFO)
                   .setText(msg))
    .build();
}
```

`accessBacklog` から呼び出されると3つの関数も実装します。

```javascript
// 課題のステータスを取得し完了であるか確認する
// 使用API https://developer.nulab-inc.com/ja/docs/backlog/api/2/get-issue/
function isCompleteStatus(id){
  var url = "https://" + spaceurl + "/api/v2/issues/" + id + "?apiKey=" + apikey;
  
  var res = JSON.parse(UrlFetchApp.fetch(url));
  
  //4は｢完了｣
  return res.status.id == "4";
}

// 課題のコメント欄にメール内容を転載
// 使用API https://developer.nulab-inc.com/ja/docs/backlog/api/2/add-comment/
function addComment(id,from,body){
  var url = "https://" + spaceurl + "/api/v2/issues/" + id + "/comments?apiKey=" + apikey;
  var name = from.replace(/"(.+)".+/,"$1"); //余計な文字を削除
  
  var param = {
    'content' :  name + "さん回答済み、クローズ。\n\n```\n" + body + "\n```" 
  };
      
  var options = {
    'method': 'POST',
    'headers' : {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    'payload': param
  };
  
  var res = UrlFetchApp.fetch(url, options);
  
  return res.getResponseCode() == '201';
}

// 課題のステータスを完了させる
// 使用API https://developer.nulab-inc.com/ja/docs/backlog/api/2/update-issue/
function completeStatus(id){
  var url = "https://" + spaceurl + "/api/v2/issues/" + id + "?apiKey=" + apikey;

  var param = {
    'statusId' : "4",  //｢完了｣にする
    'resolutionId' : "0" //｢対応済み｣にする
  };
  
  var options = {
    'method': 'PATCH',
    'headers' : {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    'payload': param
  };
  
  var res = UrlFetchApp.fetch(url, options);  
  
  return res.getResponseCode() == '200';
}
```

## 動作確認

完成したので動作してみます。テスト用にBacklogの課題を立てて、自分自身にメールを送り、アドオンからBacklogを更新してみます。

<img width="1109" alt="11.png" src="https://qiita-image-store.s3.amazonaws.com/0/221596/ec30eb70-d50a-e9d2-6c94-ca4a4fe7341c.png">

Backlogが更新されました！

<img width="1559" alt="12.png" src="https://qiita-image-store.s3.amazonaws.com/0/221596/fdf232cd-9f8d-e9d0-d10a-9e05232d9453.png">

再度更新ボタンを押すと既にクローズしているとメッセージが表示されます。

<img width="348" alt="13.png" src="https://qiita-image-store.s3.amazonaws.com/0/221596/37c945b5-d277-10cc-5390-a337828541b1.png">

## デバッグ

`Logger.log()` を入れてGmail Add-onを動かすとスクリプトエディタ側のログで見れます。

# 参考文献

以下の記事を参考にさせていただきました。ありがとうございます。

* [claspを使い、Google Apps Scriptプロジェクトをgitでバージョン管理する](https://qiita.com/rf_p/items/7492375ddd684ba734f8)
* [Google Apps Script で Gmail Add-on を作ってみよう](https://qiita.com/wezardnet/items/9d19ab6dcac7ccae00db)

またGASの基礎に関してはこの本を活用させていただきました。ありがとうございます。

* [詳解! GoogleAppsScript完全入門 ~GoogleApps & G Suiteの最新プログラミングガイド~](https://www.amazon.co.jp/%E8%A9%B3%E8%A7%A3-GoogleAppsScript%E5%AE%8C%E5%85%A8%E5%85%A5%E9%96%80-GoogleApps-G-Suite%E3%81%AE%E6%9C%80%E6%96%B0%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%82%AC%E3%82%A4%E3%83%89/dp/4798053767)

# 最後に

ハンズラボAdvent Calendar 2018 の最後は@daisukeArkさんです！