AppStore アプリのレビュー Slack ボットについて
Ruby+Fastlane-SpaceShip を使って APP レビュー追跡通知 Slack ボットを作成する

写真提供者:Austin Distel
米を食べていて米の値段を知らない

最近知ったのですが、Slackでアプリの最新レビューを転送するボットは有料でした。ずっとこの機能は無料だと思っていました。料金は月額5ドルから200ドルまであり、各プラットフォームは「App Review Bot」機能だけでなく、データ分析、記録、統合管理、競合比較なども提供しているため、料金はサービス内容によって異なります。Review Botはその一部に過ぎませんが、私はこの機能だけを使いたいので、他の機能が不要なら料金を払うのはもったいないと感じました。
問題
もともと無料オープンソースのツール TradeMe/ReviewMe を使って Slack 通知をしていましたが、このツールは長年メンテナンスされておらず、時々 Slack に古いレビューが大量に流れてきて驚かされます(多くのバグはすでに修正済みなのに、また問題があると思ってしまう!)、原因は不明です。
なので、他のツールや方法を検討することにしました。
TL;DR [2022/08/10] アップデート:
現在は新しい App Store Connect API を使って App Reviews Bot を再設計し、「ZReviewTender — 無料オープンソースの App Reviews 監視ボット」としてリニューアルしました。
====
2022/07/20 更新
App Store Connect API は現在、カスタマーレビューの読み取りと管理をサポートしています。App Store Connect API はネイティブにアプリのレビューにアクセスできるため、Fastlane — Spaceship を使ってバックエンドからレビューを取得する必要はありません。
原理探究
動機ができたら、次に目標達成の原理を研究しましょう。
公式API ❌
AppleはApp Store Connect APIを提供していますが、レビューを取得する機能はありません。
[2022/07/20 更新]: App Store Connect API はカスタマーレビューの読み取りと管理をサポートしました
Public URL API (RSS) ⚠️
Appleは公開のAPPレビュー用RSS購読URLを提供しており、rss xmlのほかにjson形式も提供しています。
https://itunes.apple.com/国コード/rss/customerreviews/id=APP_ID/page=1/sortBy=mostRecent/json
-
国コード:こちらのドキュメントを参照してください。
-
APP_ID:Appのウェブ版にアクセスすると、URLは https://apps.apple.com/tw/app/APP名前/id 12345678 のようになり、idの後の数字がApp ID(純粋な数字)です。
-
page:1〜10ページをリクエスト可能で、それ以上は取得できません。
-
sortBy:
mostRecent/jsonは最新のデータを JSON 形式でリクエストします。mostRecent/xmlに変更すると XML 形式になります。
評価データの返却は以下の通りです:
rss.json:
{
"author": {
"uri": {
"label": "https://itunes.apple.com/tw/reviews/id123456789"
},
"name": {
"label": "test"
},
"label": ""
},
"im:version": {
"label": "4.27.1"
},
"im:rating": {
"label": "5"
},
"id": {
"label": "123456789"
},
"title": {
"label": "素晴らしい存在!"
},
"content": {
"label": "人生に価値ができました~",
"attributes": {
"type": "text"
}
},
"link": {
"attributes": {
"rel": "related",
"href": "https://itunes.apple.com/tw/review?id=123456789&type=Purple%20Software"
}
},
"im:voteSum": {
"label": "0"
},
"im:contentType": {
"attributes": {
"term": "Application",
"label": "アプリケーション"
}
},
"im:voteCount": {
"label": "0"
}
}
利点:
-
公開されており認証不要でアクセス可能
-
シンプルで使いやすい
欠点:
-
この RSS API は非常に古く、まったく更新されていません。
-
評価の情報が少なすぎる(コメント日時なし、編集済みの評価?返信済み?)
-
データの乱れの問題に直面(後半のページで時々古いデータが突然表示される)
-
最大で10ページまでアクセス可能
私たちが直面した最大の問題は3ですが、この部分が私たちが使っているBotツールの問題なのか、それともこのRSS URLのデータ自体の問題なのかは不明です。
プライベートURL API ✅
この方法は少し邪道かもしれませんが、私のひらめきで見つけたものです。しかし、その後他のレビューBotのやり方を参考にしたところ、多くのサイトも同じ方法を使っていることが分かりました。問題はなさそうで、4〜5年前にも同様のツールがあったのを見たことがありますが、その時は深く調べませんでした。
利点:
-
Appleの管理画面のデータと同じです
-
データは完全かつ最新です
-
より詳細なフィルタリングが可能です
-
深く統合されたアプリツールもこの方法を使っています(AppRadar/AppReviewBot…)
欠点:
-
非公式の方法(裏技)
-
Apple は二段階認証を全面的に導入しているため、ログインセッションは定期的に更新する必要があります。
ステップ1 — App Store Connect 管理画面のレビューセクションでデータを読み込むAPIを解析:

Appleの管理画面にアクセスするには、以下のコマンドを実行します:
https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/APP_ID/platforms/ios/reviews?index=0&sort=REVIEW_SORT_ORDER_MOST_RECENT
このエンドポイントはレビューリストを取得します:

index = ページネーションのオフセット。1回で最大100件表示。
評価データの返却は以下の通りです:
private.json:
{
"value": {
"id": 123456789,
"rating": 5,
"title": "素晴らしい存在!",
"review": "人生に価値ができました〜",
"created": null,
"nickname": "test",
"storeFront": "TW",
"appVersionString": "4.27.1",
"lastModified": 1618836654000,
"helpfulViews": 0,
"totalViews": 0,
"edited": false,
"developerResponse": null
},
"isEditable": true,
"isRequired": false,
"errorKeys": null
}
また、テストした結果、cookie: myacinfo=<Token> を付けるだけでリクエストを偽造してデータを取得できることが分かりました:

APIはわかった、必要なヘッダーも把握した。次はバックエンドのこのcookie情報を自動で取得する方法を考えよう。
ステップ2 — 万能な Fastlane
因みにAppleは現在、完全な二段階認証を実施しているため、ログイン認証の自動化はより複雑になっています。幸いにも、Appleと知恵比べをしている Fastlane は、公式のApp Store Connect API、iTMSTransporter、ウェブ認証(2段階認証を含む)をすべて実装しています。私たちはFastlaneのコマンドを直接使用できます:
fastlane spaceauth -u <App Store Connect アカウント(メールアドレス)>
このコマンドはウェブログイン認証(2段階認証を含む)を完了し、その後 cookie を FASTLANE_SESSION ファイルに保存します。
以下のような文字列が得られます:
!ruby/object:HTTP::Cookie
name: myacinfo value: <token>
domain: apple.com for_domain: true path: "/"
secure: true httponly: true expires: max_age:
created_at: 2021-04-21 20:42:36.818821000 +08:00
accessed_at: 2021-04-21 22:02:45.923016000 +08:00
!ruby/object:HTTP::Cookie
name: <hash> value: <token>
domain: idmsa.apple.com for_domain: true path: "/"
secure: true httponly: true expires: max_age: 2592000
created_at: 2021-04-19 23:21:05.851853000 +08:00
accessed_at: 2021-04-21 20:42:35.735921000 +08:00
!ruby/object:HTTP::Cookie
name: dqsid value: <token>
domain: appstoreconnect.apple.com for_domain: false path: "/" secure: true httponly: true expires: max_age: 1800
created_at: &1 2021-04-21 22:02:47.118437000 +08:00
accessed_at: *1
myacinfo = value を代入するだけで評価リストを取得できます。
ステップ3 — SpaceShip
もともと Fastlane はここまでしか使えないと思っていて、あとは自分で Fastlane から cookie を取得して API を叩くフローを組む必要があると思っていました。しかし、調査してみると、Fastlane の認証モジュール SpaceShip にはさらに強力な機能が備わっていることが分かりました!

SpaceShip
SpaceShip にはすでにレビュー一覧を取得するメソッド Class: Spaceship::TunesClient::get_reviews が用意されています!
app = Spaceship::Tunes::login(appstore_account, appstore_password)
reviews = app.get_reviews(app_id, platform, storefront, versionId = '')
# アプリのレビューを取得
*storefront = 地域
第四歩 — 組み立て
Fastlane、Spaceship はどちらも Ruby で書かれているため、この Bot ツールも Ruby で作成します。
reviewBot.rb ファイルを作成し、実行する際はターミナルで以下を入力するだけです:
ruby reviewBot.rb
即可。(より詳しい Ruby 環境の問題については文末のヒントを参照してください)
まず、元の get_reviews メソッドのパラメータは私たちの要望に合わなかったため、全地域・全バージョンのレビューを取得し、フィルタリング不要でページネーションに対応したいと思います:
extension.rb:
# Spaceship->TunesClient 拡張
module Spaceship
class TunesClient < Spaceship::Client
def get_recent_reviews(app_id, platform, index)
r = request(:get, "ra/apps/#{app_id}/platforms/#{platform}/reviews?index=#{index}&sort=REVIEW_SORT_ORDER_MOST_RECENT")
parse_response(r, 'data')['reviews']
end
end
end
なので、TunesClient にメソッドを拡張し、パラメータは app_id、platform = ios(すべて小文字)、index = ページのオフセットのみを渡します。
次にログイン認証とレビューリストの取得を組み立てます:
get_recent_reviews.rb:
index = 0
breakWhile = true
while breakWhile
app = Spaceship::Tunes::login(APPStoreConnect アカウント(メール), APPStoreConnect パスワード)
reviews = app.get_recent_reviews($app_id, $platform, index)
if reviews.length() <= 0
breakWhile = false
break
end
reviews.each { \\|review\\|
index += 1
puts review["value"]
}
end
while を使ってすべてのページを繰り返し、内容がなくなったら終了する。
次に、前回の最新の日時を記録し、未通知の最新メッセージのみ通知するようにします:
lastModified.rb:
lastModified = 0
if File.exists?(".lastModified")
lastModifiedFile = File.open(".lastModified")
lastModified = lastModifiedFile.read.to_i
end
newLastModified = lastModified
isFirst = true
messages = []
index = 0
breakWhile = true
while breakWhile
app = Spaceship::Tunes::login(APPStoreConnect アカウント(Email), APPStoreConnect パスワード)
reviews = app.get_recent_reviews($app_id, $platform, index)
if reviews.length() <= 0
breakWhile = false
break
end
reviews.each { \\|review\\|
index += 1
if isFirst
isFirst = false
newLastModified = review["value"]["lastModified"]
end
if review["value"]["lastModified"] > lastModified && lastModified != 0
# 初回は通知しない
messages.append(review["value"])
else
breakWhile = false
break
end
}
end
messages.sort! { \\|a, b\\| a["lastModified"] <=> b["lastModified"] }
messages.each { \\|message\\|
notify_slack(message)
}
File.write(".lastModified", newLastModified, mode: "w+")
単純に .lastModified で前回実行時に取得した時間を記録します。
初回は通知を送らず、そうしないと一度に大量通知が送信されます
最後のステップ、通知メッセージを作成して Slack に送信:
slack.rb:
# Slack Bot
def notify_slack(review)
rating = review["rating"].to_i
color = rating >= 4 ? "good" : (rating >= 2 ? "warning" : "danger")
like = review["helpfulViews"].to_i > 0 ? " - #{review["helpfulViews"]} :thumbsup:" : ""
date = review["edited"] == false ? "Created at: #{Time.at(review["lastModified"].to_i / 1000).to_datetime}" : "Updated at: #{Time.at(review["lastModified"].to_i / 1000).to_datetime}"
isResponse = ""
if review["developerResponse"] != nil && review["developerResponse"]['lastModified'] < review["lastModified"]
isResponse = " (返信は期限切れです)"
end
edited = review["edited"] == false ? "" : ":memo: ユーザーがレビューを更新しました#{isResponse}:"
stars = "★" * rating + "☆" * (5 - rating)
attachments = {
:pretext => edited,
:color => color,
:fallback => "#{review["title"]} - #{stars}#{like}",
:title => "#{review["title"]} - #{stars}#{like}",
:text => review["review"],
:author_name => review["nickname"],
:footer => "iOS - v#{review["appVersionString"]} - #{review["storeFront"]} - #{date} - <https://appstoreconnect.apple.com/apps/APP_ID/appstore/activity/ios/ratingsResponses\\|Go To App Store>"
}
payload = {
:attachments => [attachments],
:icon_emoji => ":storm_trooper:",
:username => "ZhgChgLi iOS Review Bot"
}.to_json
cmd = "curl -X POST --data-urlencode 'payload=#{payload}' SLACK_WEB_HOOK_URL"
system(cmd, :err => File::NULL)
puts "#{review["id"]} 通知送信成功!"
end
SLACK_WEB_HOOK_URL = Incoming WebHook URL
最終結果
appreviewbot.rb:
require "Spaceship"
require 'json'
require 'date'
# 設定
$slack_web_hook = "目標通知の web hook url"
$slack_debug_web_hook = "ボットにエラーが発生した時の通知 web hook url"
$appstore_account = "APPStoreConnect アカウント(メール)"
$appstore_password = "APPStoreConnect パスワード"
$app_id = "APP_ID"
$platform = "ios"
# Spaceship->TunesClient 拡張
module Spaceship
class TunesClient < Spaceship::Client
def get_recent_reviews(app_id, platform, index)
r = request(:get, "ra/apps/#{app_id}/platforms/#{platform}/reviews?index=#{index}&sort=REVIEW_SORT_ORDER_MOST_RECENT")
parse_response(r, 'data')['reviews']
end
end
end
# Slack Bot
def notify_slack(review)
rating = review["rating"].to_i
color = rating >= 4 ? "good" : (rating >= 2 ? "warning" : "danger")
like = review["helpfulViews"].to_i > 0 ? " - #{review["helpfulViews"]} :thumbsup:" : ""
date = review["edited"] == false ? "Created at: #{Time.at(review["lastModified"].to_i / 1000).to_datetime}" : "Updated at: #{Time.at(review["lastModified"].to_i / 1000).to_datetime}"
isResponse = ""
if review["developerResponse"] != nil && review["developerResponse"]['lastModified'] < review["lastModified"]
isResponse = " (カスタマーサポートの返信が期限切れ)"
end
edited = review["edited"] == false ? "" : ":memo: ユーザーがレビューを更新しました#{isResponse}:"
stars = "★" * rating + "☆" * (5 - rating)
attachments = {
:pretext => edited,
:color => color,
:fallback => "#{review["title"]} - #{stars}#{like}",
:title => "#{review["title"]} - #{stars}#{like}",
:text => review["review"],
:author_name => review["nickname"],
:footer => "iOS - v#{review["appVersionString"]} - #{review["storeFront"]} - #{date} - <https://appstoreconnect.apple.com/apps/APP_ID/appstore/activity/ios/ratingsResponses\\|Go To App Store>"
}
payload = {
:attachments => [attachments],
:icon_emoji => ":storm_trooper:",
:username => "ZhgChgLi iOS Review Bot"
}.to_json
cmd = "curl -X POST --data-urlencode 'payload=#{payload}' #{$slack_web_hook}"
system(cmd, :err => File::NULL)
puts "#{review["id"]} send Notify Success!"
end
begin
lastModified = 0
if File.exists?(".lastModified")
lastModifiedFile = File.open(".lastModified")
lastModified = lastModifiedFile.read.to_i
end
newLastModified = lastModified
isFirst = true
messages = []
index = 0
breakWhile = true
while breakWhile
app = Spaceship::Tunes::login($appstore_account, $appstore_password)
reviews = app.get_recent_reviews($app_id, $platform, index)
if reviews.length() <= 0
breakWhile = false
break
end
reviews.each { \\|review\\|
index += 1
if isFirst
isFirst = false
newLastModified = review["value"]["lastModified"]
end
if review["value"]["lastModified"] > lastModified && lastModified != 0
# 初回利用時は通知しない
messages.append(review["value"])
else
breakWhile = false
break
end
}
end
messages.sort! { \\|a, b\\| a["lastModified"] <=> b["lastModified"] }
messages.each { \\|message\\|
notify_slack(message)
}
File.write(".lastModified", newLastModified, mode: "w+")
rescue => error
attachments = {
:color => "danger",
:title => "AppStoreReviewBot エラー発生!",
:text => error,
:footer => "*Appleの技術的制限により、正確なレビュー取得機能は約1ヶ月ごとに再ログイン設定が必要です。ご了承ください。"
}
payload = {
:attachments => [attachments],
:icon_emoji => ":storm_trooper:",
:username => "ZhgChgLi iOS Review Bot"
}.to_json
cmd = "curl -X POST --data-urlencode 'payload=#{payload}' #{$slack_debug_web_hook}"
system(cmd, :err => File::NULL)
puts error
end
また、begin…rescue(try…catch)による保護も追加しており、エラーが発生した場合はSlackに通知して確認できるようにしています(ほとんどの場合はセッションの期限切れです)。
最後に、このスクリプトを crontab や schedule などのスケジューラーに追加して定期実行するだけです!
効果図:

無料の他の選択肢
-
AppFollow :Public URL API(RSS)を使うと、まあ使えないこともない程度です。
-
feedis.io :プライベートURL APIを使用するため、アカウントとパスワードを提供する必要があります。
-
TradeMe/ReviewMe :セルフホスト型サービス(node.js)で、以前はこちらを使っていましたが、前述の問題に直面しました。
ご注意ください
1.⚠️プライベートURL APIの方法では、二段階認証が有効なアカウントの場合、最長で30日に一度は再認証が必要で、現時点では解決策がありません;二段階認証なしのアカウントを作成できれば、問題なく快適に使えます。

#important-note-about-session-duration
2.⚠️無料、有料、この記事の自作いずれの場合も、開発者アカウントを使用せず、必ず独立した App Store Connect アカウントを作成して使用してください。権限は「Customer Support」のみを付与し、セキュリティ問題を防止しましょう。
3.Ruby はシステムに標準搭載されている 2.6 版と競合しやすいため、rbenv を使って管理することをおすすめします。
- macOS CatalinaでGEMやRuby環境のエラーが発生した場合は、こちらの返信 を参考にしてください。
解決した問題!
以上の過程を経て、Slack Bot の動作方法や iOS App Store のレビュー取得方法がよく理解できました。また、Ruby にも触れてみましたが、書きやすくてとても良いです!
Post は ZMediumToMarkdown によって Medium から変換されました。



コメント