GASを使ってScrapboxの新着ページだけをSlackに通知する

## ねらい

[エンジニアのアウトプット？Scrapboxで決まりでしょ！](https://qiita.com/Tommy_/items/f881c1658dfe02f65b69)を読んで以来、情報共有サービスの[Scrapbox](https://scrapbox.io/product)を愛用しています。


Scrapboxには、Slackに更新を通知する機能があります。Scrapboxではリアルタイムで編集が反映されるため、通知タイミングは**最後の書き込みから90秒経過した時**となっています。

- [Slackに更新を通知する - Scrapbox ヘルプ](https://scrapbox.io/help-jp/Slack%E3%81%AB%E6%9B%B4%E6%96%B0%E3%82%92%E9%80%9A%E7%9F%A5%E3%81%99%E3%82%8B)

ただし、ある程度人数のいるチームで使っていると、通知のタイミングが多すぎると感じることがあります。Scrapboxの開発思想では[書いた後からどんどん修正していく](https://scrapbox.io/forum-jp/%E3%83%86%E3%83%AD%E3%83%A1%E3%82%A2%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6#59080f7497c29100008eb0c2)ことが推奨されていますが、都度全員に通知が飛んでしまうと細かく追記・修正しにくいと思う人がいるかもしれません。

そこで通知の量を減らしたいところですが、Scrapbox側の機能では細かい設定が用意されていません。代わりに、**Google Apps Script**（GAS）を間に挟んで通知をカスタマイズするというアイデアが以下の記事で紹介されています。

- [Scrapboxの更新通知をカスタムしてSlackに投げる | kitayama lab](https://kitayamalab.wordpress.com/2017/05/02/scrapbox%E3%81%AE%E6%9B%B4%E6%96%B0%E9%80%9A%E7%9F%A5%E3%82%92%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%81%97%E3%81%A6slack%E3%81%AB%E6%8A%95%E3%81%92%E3%82%8B/)

本記事では、**ページが作成されたときに限り通知**することで、通知の量を減らす方法をご紹介します。

## 環境
- Scrapbox (https://scrapbox.io)
- Google Apps Script (https://script.google.com)
- Slack (https://slack.com)

## コード
全体のコードはGistに公開しました。コピペしたい方はこちらからどうぞ。

- [kn1cht/new-page.gs
](https://gist.github.com/kn1cht/3228bacd8c35fa30675b7df226afc0ef)

## 新着ページ判定の流れ
新着ページの通知にあたって、GAS側で行っていることを解説します。

1. 事前に全ページのタイトルを保存しておく
2. （初回のみ）ScrapboxのAPIから全ページを取得する
3. Scrapboxからの通知を受け取る
4. タイトルが新しい場合はSlackに通知し、全ページタイトルに新しいタイトルを追加して保存する


### 事前に全ページのタイトルを保存しておく
GASは実行時間に制限が付いているので、データをずっと保持したい場合は外部に保存しなければなりません。Google SpreadSheetとの連携がよく使われますが、今回はタイトルを配列で持っておくだけで良いので、**JSONファイルをGoogle Driveへ保存する**ようにしました。

```js:newpage.gs
function getJsonFileFromDrive(name) {
  var files = DriveApp.searchFiles('title = "' + name + '"');
  if (files.hasNext()) return files.next();
  return DriveApp.createFile(name, '');
}
```

`name`というファイルがあれば取得し、なければ作成します。どちらにしてもFileクラスで返り、`getBlob().getDataAsString()`で内容の文字列が得られます。

```js:newpage.gs
/* ファイルから配列を取得する */
var titles = JSON.parse(getJsonFileFromDrive(jsonName).getBlob().getDataAsString() || '[]');
/* 配列をファイルに保存する */
getJsonFileFromDrive(jsonName).setContent(JSON.stringify(titles));
```

作成したJSONファイルは、自分のGoogle Driveで存在を確認できます。

<img width="400" alt="Google Drive上のjsonファイル" src="https://qiita-image-store.s3.amazonaws.com/0/150557/f01d1c99-3647-3838-286b-cb79651bea75.png">

### （初回のみ）ScrapboxのAPIから全ページを取得する
[ScrapboxにはAPIが用意されており](https://scrapbox.io/help-jp/API)、ページリストなどを取得できます。「予告なく変更を行う」とのことなので、使う際は最新の情報をご確認ください。

使い方はURLをGETするだけで、例えばhelp-jpの全ページリストは[scrapbox.io/api/pages/help-jp](https://scrapbox.io/api/pages/help-jp?limit=1000)で見ることができます。ただし、**デフォルトでは100ページまでしか取れない**ので、ご自身のプロジェクト規模によっては`?limit=1000`などとしておきましょう。

#### Privateプロジェクトの場合
公式のヘルプページには非公開プロジェクトでAPIを利用する方法が載っていませんが、こちらで解説されています。

- [nishio/ScrapboxのprivateプロジェクトのAPIを叩く](https://scrapbox.io/nishio/Scrapbox%E3%81%AEprivate%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AEAPI%E3%82%92%E5%8F%A9%E3%81%8F)

`connect.sid`が分かったら、リクエストヘッダにCookieとして設定してからAPIを呼べばよいです。

```js:newpage.gs
function getPageListFromApi() {
  var headers = { 'Cookie' : 'connect.sid=' + sid };
  var response = UrlFetchApp.fetch('https://scrapbox.io/api/pages/' + projectName + '?limit=1000', {
    method  : 'get',
    headers : headers
  });
  return JSON.parse(response);
}
```
今回欲しいのはページのタイトルだけなので、mapで取り出して配列化します。

```js:newpage.gs
if(titles.length === 0) // fetch the list of the page titles
  titles = getPageListFromApi().pages.map(function(p){ return p.title; });
```

### Scrapboxからの通知を受け取る
Scrapboxからの通知は`doPost()`関数で受け取ります。Slackに渡すことが想定されているため、通知内容の取得が少し面倒です。

```js:newpage.gs
function doPost(e) {
  var attachment = JSON.parse(e.postData.getDataAsString()).attachments[0];

  console.log(attachment.title); // ページタイトル
  // ...
}
```

### Slackへの通知とタイトルの保存
`titles`に`attachment.title`がなければ新着ページと判定し[^1]、`titles`に新しいタイトルを加え、Slackに通知を出します。

[^1]: この判定法では、ページの名称を変更した際も新着として通知が流れてしまいます。内部的にはページにIDを振っているようなので、IDも考慮すれば区別が可能だと考えられます。ただし、本記事の目的は通知を減らすことなので特に対処していません。

```js:newpage.gs
function doPost(e) {
  // ...
  if(titles.indexOf(attachment.title) === -1) { // New page!
    titles.push(attachment.title);

    var message = projectName + 'に記事"' + attachment.title + '"が作成されました';
    console.log(message);
    UrlFetchApp.fetch(webHookUrl, options(message, attachment));
  }
  getJsonFileFromDrive(jsonName).setContent(JSON.stringify(titles));
}
```

Slack通知用にリクエストを組み立てる部分は`options()`に分離しています。`username`や`icon_emoji`を設定すると通知の見た目がよくなるのでオススメです（あらかじめアイコン用にカスタム絵文字を追加しておきましょう）。

```js:newpage.gs
function options(message, attachment) {
  return {
    method      : 'post',
    contentType : 'application/json',
    payload     : JSON.stringify({
      username    : 'Scrapbox',
      icon_emoji  : ':scrapbox:',
      text        : message,
      attachments : [attachment]
    })
  };
}
```

<img width="368" alt="それっぽい通知の例" src="https://qiita-image-store.s3.amazonaws.com/0/150557/4b7705ae-6f43-9957-57dc-8429e27a6da0.png">

## 通知がGASを経由するように設定する
最後に、設定の手順を簡単に説明します。GASを使うのが初めてという場合はお読みください。

### GASの設定
コードをGASに貼り付けた段階では、このような画面になっているはずです。

<img width="944" alt="GAS画面" src="https://qiita-image-store.s3.amazonaws.com/0/150557/f3128052-46cf-67de-3768-94ecd1739a42.png">

メニューから「**ファイル**」→「**プロジェクトのプロパティ**」と選んで出てくる画面で「**スクリプトのプロパティ**」を選択します。ここに設定した文字列は、コードから読み取って使えます。

本記事のコードではScrapboxやSlackの認証情報などを保存するのに使いました。以下3つ（2つ）を設定してください。

- `SCRAPBOX_PROJECT`：通知したいScrapboxのプロジェクト名です。
- `SLACK_WEBHOOK_URL`：Slackで通知を受け取るのに使うIncoming WebhookのURLです。
- `SCRAPBOX_SID`：公開プロジェクトの場合は不要です。[privateプロジェクトの場合](#privateプロジェクトの場合)で説明した`connect.sid`を貼り付けてください。

<img width="892" alt="スクリプトのプロパティ" src="https://qiita-image-store.s3.amazonaws.com/0/150557/77978121-f1e9-5780-3430-324468f4216a.png">

特に下の2つは流出すると悪用されかねないので、扱いにはお気をつけください。

プロパティを設定したら、「**公開**」→「**ウェブアプリケーションとして導入**」に進みます。「アプリケーションにアクセスできるユーザー:」を「全員（匿名ユーザーを含む）」に変更します。

<img width="350" alt="ウェブアプリケーションとして導入" src="https://qiita-image-store.s3.amazonaws.com/0/150557/c1f4445f-af36-6e42-53a8-2da62620d48e.png">

「導入」を押すと、「**現在のアプリケーションのURL**」が表示されます。こちらはScrapboxからGASに通知を渡すのに必要なので、コピーしておきましょう。

### Scrapboxの設定
「Setting」→「Notifications」から通知をセットアップし、GASで表示されたURLを貼り付けます。既に設定済みの場合は、古い方を消して再設定しましょう。

<img width="835" alt="スクリーンショット 0031-03-16 17.43.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/150557/642cd1ed-1dfa-e32c-09be-392adf2e6ef1.png">

これでScrapbox→GAS→Slackの順に通知が流れるようになるはずです。うまく動いていない場合は、[StackDriverのログ](https://qiita.com/howdy39/items/398f4969639a558c8487)などを確認してみると切り分けができます。

## おわりに
本記事では、Scrapboxのタイトル一覧を保存して新着ページだけをSlackに通知する手法をご紹介しました。応用次第では、「○日以上編集されなかったページが編集されたら通知する」など、さらに複雑な動作も作れそうです。

皆様の快適なScrapboxライフのヒントとなれば幸いです。