iOSアプリのレビューをGASでSlackに通知するやつを作りました。

iOSアプリのレビューをGASでSlackに通知するやつを作りました。
LITALICOでアプリエンジニア（iOS/Rails）を担当しています、shuyuheyです。
この記事は『[LITALICO Engineers Advent Calendar 2017](https://qiita.com/advent-calendar/2017/litalico)』11日目の記事です。

---

# はじめに
アプリの品質を上げるためには、ユーザがどのようにアプリを使っているか、という情報を収集することは大切なことです。特に、ストアに寄せられるレビューは、アプリをこれからインストールする人が目にするものでもありますので、真摯に対応することが重要になります。

またコノビーでは、アプリ内でコメントをする機能があります。コメントが追加されたら、ユーザの声が集まるチャンネルに自動で投稿されるようになっています。

今回は、AppStoreにレビューが追加されたら、自動で内容がSlackのチャンネルに投稿されるようなGoogle Apps Scriptを作りました。

コードはGISTに置きましたので、自由にお使いください。
[AppStoreのレビューをSlackに通知する · GitHub](https://gist.github.com/shuyuhey/891b7243a011605dee8f3ecab2baccc7)

詳細は後述しますが、実行前に予めスクリプトプロパティに`last_updated` をキーとして、 `2017-01-01` などの日付を予め入れておく必要があります。

<img width="702" alt="FetchAppStoreReview.png" src="https://qiita-image-store.s3.amazonaws.com/0/56528/6d70b911-2404-8ffa-b862-aa645cbd01c2.png">


以降は、コードの一部についての解説となります。

# 方針
- Google Apps Scriptで、時間をトリガーにして実行する。
* 定期的にXMLを取得し、更新があればSlackに通知する。

Rubyなどのスクリプトをサーバにおいても良かったのですが、手軽さを優先して、Google Apps Scriptで実装しました。


# レビューを取得する
AppStoreのレビューは、WebAPIを経由して取得する事ができます。URLは下記の通りです。

```
https://itunes.apple.com/jp/rss/customerreviews/id={アプリのID}/xml
```

例えば、コノビーならば、下記のURLでレビューを取得することができます。

> https://itunes.apple.com/jp/rss/customerreviews/id=1080525142/xml


```xml:レスポンス一部抜粋
<entry>
  <updated>2017-11-25T22:22:02-07:00</updated>
  <id>XXXXXXX</id>
  <title>レビュータイトル</title>
  <content type="text">レビューコメント</content>
  <im:contentType term="Application" label="アプリケーション"/>
  <im:voteSum>0</im:voteSum>
  <im:voteCount>0</im:voteCount>
  <im:rating>5</im:rating>
  <im:version>1.10.0</im:version>
  <author>
    <name>ユーザ名</name>
    <uri>https://itunes.apple.com/jp/reviews/id523347250</uri>
  </author>
  <link rel="related" href="https://itunes.apple.com/jp/review?id=1080525142&type=Purple%20Software"/>
  <content type="html">
    <table border="0" width="100%"> <tr> <td> <table border="0" width="100%" cellspacing="0" cellpadding="0"> <tr valign="top" align="left"> <td width="100%"> <b><a href="https://itunes.apple.com/jp/app/XXXX/id1080525142?mt=8&uo=2">レビュータイトル</a></b><br/> <font size="2" face="Helvetica,Arial,Geneva,Swiss,SunSans-Regular"> </font> </td> </tr> </table> </td> </tr> <tr> <td> <font size="2" face="Helvetica,Arial,Geneva,Swiss,SunSans-Regular">レビューコメント</font><br/> </td> </tr> </table>
  </content>
</entry>
```

上記のURLは、xmlでの取得ですが、末尾の `xml` を `json` に変更すれば、jsonでレビューを取得することも可能です。

ただし、jsonの場合は取得できるデータがxmlに比べて少ないことに注意してください。レビューを投稿した時間はjsonには含まれていませんでした。
今回は、レビューの投稿時間も欲しかったのでxmlを利用しました。

# Google Apps ScriptでXmlから値を取得する
ストアレビューのXMLをパースするときに注意するのは、namespaceです。
レビューのXMLには、http://www.w3.org/2005/Atom で定義されている標準的なタグと、http://itunes.apple.com/rss で定義されているAppleが定義したタグが混在しており、それを接頭辞で区別しています。
XMLの仕様における接頭辞についてはここでは説明を割愛します。

つまり、XML内のタグには標準的に定義されているタグとAppleが定義したタグが存在するので、値を取得するときにはどちらのnamespaceのタグなのかを明示して取得する必要があります。

Google Apps Scriptでは、 `XmlService` というxmlを扱うためのライブラリが有りますので、それを利用してnamespaceを取得することができます。
ここで指定したnamespaceは、値を取得するときに必要となります。

```javascript
  var response = UrlFetchApp.fetch(TARGET_RSS_URL);
  var xml = XmlService.parse(response.getContentText());
  var namespace = xml.getRootElement().getNamespace();
  var appleNameSpace = xml.getRootElement().getNamespace("im");
```


実際に値を取得するときには、下記のように取得します。

```js
var rating = entry.getChild("rating",appleNameSpace).getText();
var updated = entry.getChild("updated", namespace).getText();
```

`rating` というタグは、Appleが定義したものなので `appleNameSpace` を指定していて、 `updated` は標準的なタグなので `namespace`  を指定しています。

# 更新があったときだけSlackに通知する
今回のスクリプトは、時間をトリガーとして実行します。しかし、実行されるたびに全てのレビューがチャンネルに流れることは避けたいので、前回の通知以降に投稿されたレビューのみに絞って通知します。そのためには、値を何処かに保持しておく必要があります。

値の保存のために、Spreadsheetを使うこともありますが、今回はスクリプトプロパティを使うことにします。

今回書いたスクリプトで、スクリプトプロパティをにアクセスしているのは下記の部分です。

```javascript
function saveLastUpdated(today) {
  PropertiesService.getScriptProperties().setProperty('last_updated', today.toString());
}

function fetchLastUpdated() {
  var lastUpdated = PropertiesService.getScriptProperties().getProperty('last_updated');
  return Moment.moment(lastUpdated);
}
```

スクリプトプロパティは、キーバリューでデータを保存できます。保存されるデータは文字列になりますので、数値にしろJSONにしろタイムスタンプにしろ、変換が必要となります。
なお今回は、タイムスタンプの扱いを簡単にするために、Moment.jsを利用しています。

処理の一部を抜粋します。

```javascript
 var entries = xml.getRootElement().getChildren("entry", namespace);
  var today = Moment.moment();
  var lastUpdated = fetchLastUpdated();

  var reviews = entries
  .filter(function(entry, index) {
    var updated = new Date(entry.getChild("updated", namespace).getText());
    Logger.log(updated);
    return index !== 0 && updated >= lastUpdated;
  })

// 省略...
  
  if (reviews.length <= 0) { return; }
  postSlack(reviews);
  
  saveLastUpdated(today);
```

スクリプトプロパティから前回チャンネルに通知した時間を取得して、XMLに含まれるレビューの更新日時と比較します。前回に通知した時間よりあとに更新されたレビューのみを抽出して、Slackに通知をする処理をしています。

通知が終わったら、スクリプトプロパティに記録してある時間をアップデートします。

注意点ですが、このスクリプトでは、スクリプトプロパティは最初はセットされていないので、初回は手動でセットする必要があります。

本当であれば、キーがなければ適当な値をセットする処理などを入れておくべきなのですが、本当に一番最初しか使わなかったので、今回は入れませんでした。


# スクリプトの実行
スクリプトの実行は、時間をトリガーにして定期的におこなっています。コノビーの場合は、1時間に何個もレビューがつくことはないので、1日に2回スクリプトが実行されるようになっています。

# おわりに
AppStoreのレビューを取得するGoogle Apps Scriptを紹介しました。なんやかんや、GASは手軽なので最近は通知する系はさくっとGASで作ってしまうことが多いです。

---

明日は、@AquaLampの「分散システムで読み書きの順序一貫性を担保する仕組みがエモい」話です。お楽しみに。