Google Apps Scriptを用いてライブラリを使わずにSlackにGoogleサイトの変更通知を投稿する

# はじめに
最近社内の一部でSlackを使うようになり、BOTに非常に興味がある人です。
(今度人工無能作りたいですね。)
ことの発端から話すと長くなるのでやめました。

- 編集権限のない人にGoogle サイトの変更を自動で通知したい
- Google Apps ScriptでSlackに投稿したい
- でもライブラリに頼らずに作りたい

この記事ではそんな条件の人に役立つはずです。

# 今回作るもの
タイトルの通り、Googleサイトの変更通知をSlackに投稿するシステムを作ります。
私が作ったのはこんな感じのものです。
(所々載せられない部分があるので伏せています。
また、実際には枠は表示されません。)
![完成品.png](https://qiita-image-store.s3.amazonaws.com/0/76523/099dd390-6be4-ff03-979f-e9c2e68acedd.png)
ページ/ページパス部分にファイル名が入る場合もあります。
アイコンは考えるのが面倒だったので設定していません。
本記事ではこれの作り方を紹介したいと思います。

# システム概要

Googleサイトには多分APIがないので、Googleサイトからの変更通知メールを定期的にGoogle Apps Script(以下GAS)で確認しSlack APIを用いてSlackに通知するシステムを作ります。
なので、事前にGoogleサイトから変更通知を受け取れるようにしておいてください。
方法がわからなければ以下を読んでください。
[サイトやページが変更されたときに通知を受け取る - サイト ヘルプ](https://support.google.com/sites/answer/90597?hl=ja)
また、メールについては未読のものを通知対象とし、Slackに通知したらシステムで既読にします。
実行に失敗した場合はエラーメールを送信するようにもします。

# 作成手順
以下の手順で説明していきます。

1. Tokenを取得する
2. プロジェクトを作成する
3. [テスト]GASを用いてSlackにメッセージを投稿してみる
4. [テスト]Slackにリッチなメッセージを投稿してみる
5. Googleサイトからの変更通知メールをGASで取得する
6. 変更通知メールから必要な情報を抜き出す
7. メール取得部分を書き換える
8. 抜き出した情報を整形してSlackに投稿する
9. 実行に失敗したらエラーメールを送信する

# 変更通知システムの作成

## 1. Tokenを取得する
Slack APIを利用するためにはTokenが必要です。
Tokenは以下のところから取得できます。
[OAuth Tokens for Testing and Development | Slack](https://api.slack.com/docs/oauth-test-tokens)

## 2. プロジェクトを作成する
GAS単体のプロジェクトを作成します。
実行ログを残すためにスプレッドシートを作成することも考えましたが、エラー時に自分へメールすれば問題なさそうなので。

## 3. [テスト]GASを用いてSlackにメッセージを投稿してみる
ただ送るだけならこんな感じでいけます。

```js:Slackにメッセージを投稿
function postMessage() {
  var token = "取得したトークン";
  var channel = "bot-test"; //投稿先チャンネル名
  var username = "testbot"; //BOTの名前
  var text = "test" //メッセージ
  UrlFetchApp.fetch("https://slack.com/api/chat.postMessage?token=" + token + "&channel=%23" + channel + "&username=" + username + "&text=" + text);
}
```

実行するとこんな感じで投稿されます。
(初回実行時のみ外部接続を許可するか聞かれます)
![ただ投稿する.png](https://qiita-image-store.s3.amazonaws.com/0/76523/42d7e246-383d-d88b-38ac-5055b00d7664.png)

API経由の投稿はBOT扱いなので、名前の横に`BOT`と表示されます。
今回はアイコンを設定していないのですが、URLを用いて画像を指定したり、Slackで使える絵文字を指定することもできます。
詳しくは公式ドキュメントを読んでみてください。
[chat.postMessage method | Slack](https://api.slack.com/methods/chat.postMessage)

## 4. [テスト]Slackにリッチなメッセージを投稿してみる
先ほどのやり方ではシンプルすぎるメッセージなので、内容の投稿はできますがリンクはURLべた書きでしかできません。
そこで上記のドキュメントを読んでいたところ、`attachments`と言うのを見つけました。
とても気になったので調べたところ、それを使えばメッセージをリッチにできることがわかりました。
ちょうど似たようなことをやっている記事もありました。
[GASで監視メールをSlackに流す - Sanwa Systems Tech Blog](http://tech.sanwasystem.com/entry/2015/04/28/143541)
そちらもライブラリを使っていませんが、私の書き方が悪いのかそのままではattachmentsが無視されてしまいました。
試行錯誤した結果、とりあえず以下のコードでリッチなメッセージを投稿できました。
細かく変数に分ける必要はないかもしれませんが、とりあえず`JSON.stringify()`はattachementのみがいいようです。

```js:Slackにリッチなメッセージを投稿
function postRichMessage(message) {
  var token = "取得したトークン";
  var channel = "bot-test"; //投稿先チャンネル名
  var username = "testbot"; //BOTの名前
  var text = "こちらはGASを用いてリッチなメッセージを送信するテストです"; //メッセージ
  var attachments = JSON.stringify([
      {
        color: "#89CEEB", //インデント線の色
        pretext: "pretext : " + text, //その外のメッセージ
        author_name: "author_name : sakaguchi",//インデント内に表示される著者名
        author_link: "http://google.co.jp/",//そのリンク
        title: "title : " + text,//インデント内に表示されるタイトル
        title_link: "https://google.co.jp/",//そのリンク
        text: "attachments-text : *リンク先はGoogleです" //インデント内に表示されるテスト
      }
    ]);
  var payload = {
    "channel" : channel, //通知先チャンネル名
    "text" : "text : " + text, //送信テキスト
    "username": username, //BOTの名前
    "attachments": attachments //リッチなメッセージを送る用データ
  };
  var option = {
    "method" : "POST", //POST送信
    "payload" : payload //POSTデータ
  };
  UrlFetchApp.fetch("https://slack.com/api/chat.postMessage?token=" + token, option);
}
```

するとこんな感じになります。
<img width="593" alt="リッチなメッセージを投稿.png" src="https://qiita-image-store.s3.amazonaws.com/0/76523/89680a62-b3b1-2c39-db34-79ed520fd4ca.png">
テストのために同じ文面を使いまわしたので3つもありますが、とりあえずメッセージがリッチになりました。
ちなみに、Slackから通知に表示されるメッセージはattachmentsじゃない方のtextに書いた内容のみのようです。

メッセージの挙動確認はこちらでできます。
[Formatting | Slack](https://api.slack.com/docs/formatting/builder?msg=%7B%22text%22%3A%22I%20am%20a%20test%20message%20http%3A%2F%2Fslack.com%22%2C%22attachments%22%3A%5B%7B%22text%22%3A%22And%20here%27s%20an%20attachment!%22%7D%5D%7D)
JSON形式で定義するのがセオリーのようです。

## 5. Googleサイトからの変更通知メールをGASで取得する

GASにあるGmailAppを用いて未読の変更通知メールを取得します。
**2016/04/22 スレッドの取得順が新しい順だったので、古い順になるよう修正**

```js:未読の変更通知メールをGASで取得する
function getUnreadNotificationMail() {
  var unReadThreads = GmailApp.search("from:noreply@google.com is:unread");
  for (var n = unReadThreads.length -1; n >= 0; n--){
    var mails = unReadThreads[n].getMessages();
    mails.forEach(function(mail, index, array){
      if (mail.isUnread()){
        /*ここで未読のメールを元にSlackに通知したりする
        * 詳しくは後述*/
      }
    });
  }
}
```

(初回実行時のみメール操作を許可するか聞かれます)
search関数の第一引数はGMailの検索欄と同様の書き方ができます。
上のコードではfromで検索していますが、実際使っているコードはフィルタリングによりラベルを設定しているので、そちらで検索しています。
その他説明は公式ドキュメントを読んでください。
[Class GmailApp  |  Apps Script  |  Google Developers](https://developers.google.com/apps-script/reference/gmail/gmail-app#methods)
すでにネストが深いように思いますが、手抜きでこのまま行きます。
(分けたときにどういう名前にすればいいか悩む、というのもありますが。)

## 6. 変更通知メールから必要な情報を抜き出す
変更通知メールは例えば以下のようなフォーマットになっています。

```text:変更通知メール(一部)
件名
[サイト名] ページ が更新されました
本文
名前 さんがページ ページパス を更新しました。下記の変更内容を確認してください。

カラー キー: 挿入 | 削除
```

Googleサイトからの変更通知メールはその下にある差分表示が便利だと感じているのですが、それをSlackに投稿するのは面倒なので本システムでは本文の「〜しました。」までを利用します。
(Slackがhtml形式をサポートしていれば差分表示も楽なのですが・・・)
本文は次のようなhtml形式になっています。

```html:本文(一部)
<p>
        名前 さんがページ <a href="http://sites.google.com/a/以下略" target="_blank">ページパス</a> を更新しました。下記の変更内容を確認してください。
        <p>カラー キー:
          <span style="background-color:yellow;border:1px dotted;text-decoration:none">挿入</span> |
          <span style="color:red;text-decoration:line-through">削除</span>
        </p>
```

このメールから通知に必要な情報を抽出します。
抽出した情報を戻り値にしたいので、オブジェクトにまとめました。

```js:情報抽出
function extractBody(body) {
  if (body.indexOf("しました") == -1) {
    /*異なるフォーマットのメールが届いたことをメール
    詳しくは後述*/
    sendNewFormat(body);
    return null;
  }
  //本文から必要な範囲を取り出す
  return body.substring(0,body.indexOf("しました") + "しました".length);
}

function createItems(mail) {
  var message = extractBody(mail.getBody());
  if (message == null) {
    return null;
  }
  //通知に必要な情報をオブジェクトにまとめる
  var items = {};
  items.text = mail.getSubject().substring("[サイト名] ".length) + "\r" + mail.getDate().toLocaleString(); //年/月/日 時:分:秒 JST形式になる
  items.author_name = message.substring(message.indexOf(">") + 1, message.indexOf("さん") + "さん".length);
  items.title = message.substring(message.indexOf(">", 80) + 1, message.indexOf("<", 80)) + message.substring(message.lastIndexOf("を"));
  items.title_link = message.substring(message.indexOf("http://sites.google.com/a/"), message.indexOf("\"", message.indexOf("http://sites.google.com/a/")));
  return items;
}
```

抽出もまとめる部分もほぼ決め打ちですが、変更通知メールは決まったフォーマットで送られてくるようなので問題ないです。
(普通なら正規表現などを用いていい感じに書くんだと思うんですけど、私は正規表現苦手なので。)
`items.title`部分にある80という数字は、`<p>名前 さんがページ <a href="http://sites.google.com/a/以下略"`あたりが80文字だったからです。
そのaタグの`>`の位置を知るために80以降から探してもらうようにしています。
`サイト名`の部分もそうですが、メールに応じて適宜調整してくだい。

## 7. メール取得部分を書き換える
5で取得した未読メールを先ほどの`createItems`関数に渡して情報を抽出してもらい、それを後述の`postNotice`関数に渡して通知してもらうように書き換えます。

```js:メール取得部分
function getUnreadNotificationMail() {
  var unReadThreads = GmailApp.search("from:noreply@google.com is:unread");
  unReadThreads.forEach(function(unReadThread, index, array) {
    var mails = unReadThread.getMessages();
    mails.forEach(function(mail, index, array){
      if (mail.isUnread()){
        var items = createItems(mail);
        if (items != null) {
          /*Slackに投稿して結果を受け取る
          * 投稿部分は後述*/
          var result = JSON.parse(postNotice(items));
          if (result.ok) {
            mail.markRead();
          } else {
            /*投稿に失敗したことをメールする
            * 詳しくは後述*/
            sendFaild(result);
          }
        }
      }
    });
  });
}
```

`postNotice`関数は投稿結果を戻り値にするように設計します。
例えば投稿に成功するとSlack APIより次のようなJSONが取得できるので、そのok部分で成功と失敗を判別し、後述の`sendFaild`関数で失敗をメールします。

```json:投稿成功
{channel=チャンネルID, ok=true, message=投稿データ}
```

## 8. 抜き出した情報を整形してSlackに投稿する
`posetMessage`関数は4に書いた関数と似たような感じですが、先ほど作成したitemsを元にしてSlackに投稿します。

```js:整形してSlackに投稿
function postMessage(items) {
  var token = "取得したトークン";
  var channel = "チャンネル名";
  var username = "サイト名変更通知システム"; //BOTの名前
  var attachments = JSON.stringify([
      {
        color: "#89CEEB", //インデント線の色
        author_name: items.author_name,　//インデント内に表示される著者名
        title: items.title ,//インデント内に表示されるタイトル
        title_link: items.title_link,//そのリンク
        text: "上記リンクをクリックすると対象のページやファイルを表示します。" //インデント内に表示されるテキスト
      }
    ]);
  var payload = {
    "channel" : channel, //通知先チャンネル名
    "text" : items.text, //送信テキスト
    "username": username, //BOTの名前
    "attachments": attachments //リッチなメッセージを送る用データ
  };
  var option = {
    "method" : "POST", 
    "payload" : payload
  };
  return UrlFetchApp.fetch("https://slack.com/api/chat.postMessage?token=" + token, option);
}
```

投稿されるのは今回作るものに書いた以下のようなものです。
![完成品.png](https://qiita-image-store.s3.amazonaws.com/0/76523/099dd390-6be4-ff03-979f-e9c2e68acedd.png)

## 8. 実行に失敗したらエラーメールを送信する
もし何らかの理由で実行に失敗した場合はその旨をメールするようにします。

```js:エラーメール送信
function sendFaild(result) {
  sendMail("通知の投稿に失敗しました。", result);
}

function sendNewFormat(body) {
  sendMail("新しい形式があったようです。", body);
}

function sendMail(subject, message) {
  var mailAddress = "メールアドレス";
  var body = "メッセージの送信に失敗しました。";
  GmailApp.sendEmail(mailAddress<img width="852" alt="トリガー.png" src="https://qiita-image-store.s3.amazonaws.com/0/76523/4e48086a-a640-e066-4c4a-12703b5e4fa9.png">
, subject, body);
}
```

これ+トリガーの実行失敗メールにより、もしシステムが上手くいかなくても気が付きやすくなります。

## 9. トリガーを作成する
最後に自動でシステムを実行するためのトリガーを作成します。
残念ながら`メールが届いた時`という条件はないので、`1時間ごと`に実行するように設定します。
実行してもらうのは`getUnreadNotificationMail`関数です。
<img width="852" alt="トリガー.png" src="https://qiita-image-store.s3.amazonaws.com/0/76523/48985da7-d0ee-82b5-7962-70e37ece4b5e.png">


# 終わりに
説明下手な部分もあるかもしれませんが、コードはほぼコピペで動くと思います。
もし分からない部分などがあれば遠慮せずコメントください。

Qiitaに書こうと思って下書き止まりなものが幾つかあるので、いつかそれらも記事として投稿したいです。

# 参考
本システムを作るために下記ページを参考にしました。
ありがとうございました。
[Slack APIを使用してメッセージを送信する - Qiita](http://qiita.com/rubytomato@github/items/6558bfdb37d982891c09)
[Slack WebAPIでナイスなフォーマットのメッセージを送る - ハウテレビジョン開発者ブログ](http://blog.howtelevision.co.jp/entry/2015/04/09/124032)
[GASで監視メールをSlackに流す - Sanwa Systems Tech Blog](http://tech.sanwasystem.com/entry/2015/04/28/143541)
[chat.postMessage method | Slack](https://api.slack.com/methods/chat.postMessage)
[Message Attachments | Slack](https://api.slack.com/docs/attachments)
[Formatting | Slack](https://api.slack.com/docs/formatting/builder?msg=%7B%22attachments%22%3A%5B%7B%22fallback%22%3A%22Required%20plain-text%20summary%20of%20the%20attachment.%22%2C%22color%22%3A%22%2336a64f%22%2C%22pretext%22%3A%22Optional%20text%20that%20appears%20above%20the%20attachment%20block%22%2C%22author_name%22%3A%22Bobby%20Tables%22%2C%22author_link%22%3A%22http%3A%2F%2Fflickr.com%2Fbobby%2F%22%2C%22author_icon%22%3A%22http%3A%2F%2Fflickr.com%2Ficons%2Fbobby.jpg%22%2C%22title%22%3A%22Slack%20API%20Documentation%22%2C%22title_link%22%3A%22https%3A%2F%2Fapi.slack.com%2F%22%2C%22text%22%3A%22Optional%20text%20that%20appears%20within%20the%20attachment%22%2C%22fields%22%3A%5B%7B%22title%22%3A%22Priority%22%2C%22value%22%3A%22High%22%2C%22short%22%3Afalse%7D%5D%2C%22image_url%22%3A%22http%3A%2F%2Fmy-website.com%2Fpath%2Fto%2Fimage.jpg%22%2C%22thumb_url%22%3A%22http%3A%2F%2Fexample.com%2Fpath%2Fto%2Fthumb.png%22%7D%5D%7D)
[Class GmailApp  |  Apps Script  |  Google Developers](https://developers.google.com/apps-script/reference/gmail/gmail-app#methods)