Google App ScriptでQiitaのテンプレートから記事を投稿するSlackのスラッシュコマンドをつくる

GASでスラッシュコマンドをサクッとつくれるとTwitterで見たので自分もやってみました。
ネタBotでもよかったのですがそっちのセンスがないので真面目なやつをつくりました。

今回始めてGASに触れたのですが、サーバーレスのような実行環境とソースコード管理機能をまとめて提供しており、ちょっとしたWebアプリケーションを簡単につくることができとっても便利！:hugging:

# つくったもの

Slackから、Qiita:Teamのテンプレートを指定して記事を作成してくれるというシンプルなスラッシュコマンド。
チームで振り返り記事を作成する際に使っています。

|||
|:--:|:--:|
|Slackからコマンドを実行|<img width="311" alt="スクリーンショット 2020-02-24 17.34.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/740d7ba3-f86e-b8be-a254-ed3863ca62b1.png">|
|しばらくすると記事が作成された通知が来る|<img width="488" alt="スクリーンショット 2020-02-24 17.33.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/efdb940d-f26d-b889-e351-a29b1f192355.png">|


# 全体の処理の流れ

スラッシュコマンドは3秒以内にレスポンスが帰ってこないとエラーを返す仕様ですが、今回は記事作成が3秒を超えてしまう場合があったため非同期で記事を投稿するようにしました。
なので、記事の作成が最大1分ほどかかるちょっと残念な実装となってます。:sweat_smile:

![QiitaAPI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/81684404-08b3-c5dd-e01c-27a8955c1544.png)

1. Slackからコマンドを発行
2. `doPost()`がコマンドを受ける
3. `addJobQueue()`がコマンド内容をテキストでCacheServiceにキューイング
4. キューに追加された時点で一度処理を受け付けた旨をSlackに返す
5. トリガーが1分に1回実行され、キューがあれば取り出す（なければここで終了）
6. CacheServiceからコマンドを取得し、指定されたテンプレートのQiita記事を作成する
7. 作成された記事のタイトルとURLをSlackにPostする

# ざっくり解説

- [ソースコード](https://github.com/higuching/qiita_command)

## doPost()関数

```javascript
function doPost(e) {
  var verificationToken = e.parameter.token;
  if (verificationToken != PropertiesService.getScriptProperties().getProperty('SLACK_VERIFICATION_TOKEN')) {
    throw new Error('Invalid token');
  }
  
  var [command, id] = e.parameter.text.split(' ');
  switch(command) {
    case 'template':
      addJobQueue(command, id, e.parameter.channel_id);
      var response = { text: 'テンプレートから記事を作成します:cattyping:' };
      break;
    case 'template_list':
      result_message = templateList();
      var response = { text: result_message };
      break;
    default:
      var response = { text: 'なぞのコマンド:catsurprise:' };
      break;
  }

  return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
}
```

POSTリクエストが来た際に呼ばれる関数。

許可されたリクエストのみ受け付けるため、tokenのチェックを行っています。
tokenは以下から取得します。
https://api.slack.com/apps

- 対象のAppを選択=>下の方にある「Verification Token」の内容をコピー。
- GASの「ファイル」=>「プロジェクトのプロパティ」で「SLACK_VERIFICATION_TOKEN」というプロパティを追加しコピーした値を貼り付ける


|<img width="371" alt="スクリーンショット 2020-02-24 16.19.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/1156649c-d0a4-d543-a580-4600dd2fa4ec.png">|
|:-:|


|<img width="724" alt="スクリーンショット 2020-02-24 17.10.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/bb4b9045-bb52-5653-1fb6-de6f5ecda9d9.png">|
|:-:|

`/qiita template 1`というコマンドの場合、`e.parameter.text.split(' ')`とすることで`["template", "1"]`と配列形式で取得できます。
あとはテキストを判別して処理を実行させます。

(ソースコードのcase文になるtemplate_listコマンドは登録されているテンプレート一覧を返すもの。単純なので割愛します。)

## addJobQueue()

[こちら](https://qiita.com/zensai3805/items/1dc8e6f1be0499add5a6)の方のソースをそのまま使わせていただきました。
（こちらの記事が今回大いに参考になりました！ありがとうございます！）

CacheServiceはテキストしか保存できないので、必要な情報を`;`で区切りテキスト形式でまとめています。
（保存しているものは、コマンド、ID、Slackで通知するID）

これでSlackに一旦処理を受け付けた旨を返します。

## トリガー(cron)

GASのトリガーはUnixでいうところのcronのような動きをしてくれる機能で、一定間隔で関数呼び出しを行ってくれます。
今回はできるだけ早くレスポンスを返したいので呼び出し間隔を1分に設定し、`timeDrivenFunction()`を実行するよう指定します。

トリガーは以下から追加できます。

|<img width="448" alt="スクリーンショット 2020-02-24 17.44.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/ec2fb5f5-0768-3c84-6ffe-490d93dec95b.png">|
|:--:|

## createArticleFromTemplate()

テンプレートを取得し、記事を投稿する処理をまとめています。
それぞれ説明します。

### template()

指定のテンプレートをQiita APIで取得します。

```javascript
function template(template_id) {
  var url = getQiitaUrl() + "/api/v2/templates/:template_id".replace(':template_id', template_id);
  var options = {
     "method" : "get",
     "headers" : headers(),
     "muteHttpExceptions": true
  };
  var response = UrlFetchApp.fetch(url, options);
  var text = response.getContentText();
  
  return replaceDate(text);
}
```

`UrlFetchApp`を使ってGETでリクエストします。
今回はQiita:TeamのURLを使っていてプロパティに保存していましす。

```javascript

function getQiitaUrl() {
  PropertiesService.getScriptProperties().getProperty('QIITA_URL')  
}
```

Qiita APIで必要なトークンはQiita:Teamの設定から「個人用アクセストークン」で取得できます。
こちらもプロパティに保存します。

```javascript
function headers() {
  var token = PropertiesService.getScriptProperties().getProperty('QiITA_ACCESS_TOKEN');
  return {
    "Authorization": "Bearer " + token
  };
}
```

## createArticle()

取得したテンプレートを分解し、記事データを作成します。
細かい仕様については[Qiita API v2 documentation - Qiita:Developer](https://qiita.com/api/v2/docs)を参照してください。

```javascript
function createArticle(template_json) {
  var obj = JSON.parse(template_json);
  var article = {
    "body" : obj.body,
    "coediting" : true,
    "group_url_name" : null,
    "private": false,
    "tags": obj.expanded_tags,
    "title": obj.title,
    "tweet": false
  };
  return article;
}
```

## post()

記事の投稿を行います。

```javascript
function post(article_json) {
  var url = getQiitaUrl() + "/api/v2/items";

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

  var response = UrlFetchApp.fetch(url, options);
  if (response.getResponseCode() >= 300) {
    return "記事の作成に失敗したよ:man-gesturing-no:\n" + response.getContentText();
  }
  var obj = JSON.parse(response.getContentText());
  return "記事を作成したよ:man-raising-hand:\n" + obj.title + "\n" + obj.url + "";
}
```

ヘッダーはテンプレート取得時に使用したものと同様です。
HTTP Response Statusによってメッセージを切り替えています。

## postSlackMessage()

結果をSlackにPostします。

```javascript
function postSlackMessage(message, channelId) {
  var token = PropertiesService.getScriptProperties().getProperty('SLACK_LEGACY_TOKEN');
 
  var slackApp = SlackApp.create(token);
  var options = {
    username: "Qiita Post Article"
  }
  
  slackApp.postMessage(channelId, message, options);
}
```

[Slackのライブラリ](https://qiita.com/soundTricker/items/43267609a870fc9c7453)を使わせてもらっています。
こちらのライブラリでレガシートークンが必要だったので[こちらから取得](https://api.slack.com/legacy/custom-integrations/legacy-tokens)してプロパティに登録しておきます。

ライブラリは以下のように追加します。

|<img width="521" alt="スクリーンショット 2020-02-24 17.25.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/0a9efff8-d840-4539-3e07-362ae8a893c2.png">|
|:--:|

|<img width="715" alt="スクリーンショット 2020-02-24 17.25.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/13df7813-2fde-0135-d1dc-32f9c1703fb9.png">|
|:--:|

(図書館て...w)

# 参考
- [Slash CommandsとGASでSlackのオリジナルコマンドをつくる](https://qiita.com/chikuwa111/items/7a1a349b82318a5861cc)
- [Webhookで起動したGAS(Google AppsScript)の応答を非同期処理で高速化する](https://qiita.com/zensai3805/items/1dc8e6f1be0499add5a6)
- [Qiita API v2 documentation - Qiita:Developer](https://qiita.com/api/v2/docs)