Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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


はじめに

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

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

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

コードはGISTに置きましたので、自由にお使いください。
AppStoreのレビューをSlackに通知する · GitHub

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

FetchAppStoreReview.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

レスポンス一部抜粋
<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での取得ですが、末尾の xmljson に変更すれば、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は、値を取得するときに必要となります。

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

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

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

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

更新があったときだけSlackに通知する

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

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

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

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を利用しています。

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

 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の「分散システムで読み書きの順序一貫性を担保する仕組みがエモい」話です。お楽しみに。

shuyuhey
アプリエンジニアやってます。iOS (Swift)とRailsとかが今は中心。
litalico
「障害のない社会をつくる」というビジョンに向けて、社会の側にある障害をテクノロジーの力で取り除くことを目指す
http://litalico.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした