ZhgChg.Li

AppStoreレビュー監視Bot|Slack連携で評価通知を自動化

AppStoreのアプリ評価を効率的に追跡したい開発者向けに、Slack連携のレビュー通知Botで評価変動を即時把握。手動確認の手間を削減し、ユーザー声を迅速に活用可能。

AppStoreレビュー監視Bot|Slack連携で評価通知を自動化
本記事は AI による翻訳です。お気づきの点があればお知らせください。

AppStore アプリのレビュー Slack ボットについて

Ruby+Fastlane-SpaceShip を使って APP レビュー追跡通知 Slack ボットを作成する

Photo by Austin Distel

写真提供者:Austin Distel

米を食べていて米の値段を知らない

AppReviewBot の例

AppReviewBot の例

最近知ったのですが、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"
  }
}

利点:

  1. 公開されており認証不要でアクセス可能

  2. シンプルで使いやすい

欠点:

  1. この RSS API は非常に古く、まったく更新されていません。

  2. 評価の情報が少なすぎる(コメント日時なし、編集済みの評価?返信済み?)

  3. データの乱れの問題に直面(後半のページで時々古いデータが突然表示される)

  4. 最大で10ページまでアクセス可能

私たちが直面した最大の問題は3ですが、この部分が私たちが使っているBotツールの問題なのか、それともこのRSS URLのデータ自体の問題なのかは不明です。

プライベートURL API ✅

この方法は少し邪道かもしれませんが、私のひらめきで見つけたものです。しかし、その後他のレビューBotのやり方を参考にしたところ、多くのサイトも同じ方法を使っていることが分かりました。問題はなさそうで、4〜5年前にも同様のツールがあったのを見たことがありますが、その時は深く調べませんでした。

利点:

  1. Appleの管理画面のデータと同じです

  2. データは完全かつ最新です

  3. より詳細なフィルタリングが可能です

  4. 深く統合されたアプリツールもこの方法を使っています(AppRadar/AppReviewBot…)

欠点:

  1. 非公式の方法(裏技)

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

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 などのスケジューラーに追加して定期実行するだけです!

効果図:

無料の他の選択肢

  1. AppFollow :Public URL API(RSS)を使うと、まあ使えないこともない程度です。

  2. feedis.io :プライベートURL APIを使用するため、アカウントとパスワードを提供する必要があります。

  3. TradeMe/ReviewMe :セルフホスト型サービス(node.js)で、以前はこちらを使っていましたが、前述の問題に直面しました。

  4. JonSnow:セルフホスティングサービス(GO製)、Herokuへのワンクリックデプロイ対応、作者:saiday

ご注意ください

1.⚠️プライベートURL APIの方法では、二段階認証が有効なアカウントの場合、最長で30日に一度は再認証が必要で、現時点では解決策がありません;二段階認証なしのアカウントを作成できれば、問題なく快適に使えます。

#important-note-about-session-duration

#important-note-about-session-duration

2.⚠️無料、有料、この記事の自作いずれの場合も、開発者アカウントを使用せず、必ず独立した App Store Connect アカウントを作成して使用してください。権限は「Customer Support」のみを付与し、セキュリティ問題を防止しましょう。

3.Ruby はシステムに標準搭載されている 2.6 版と競合しやすいため、rbenv を使って管理することをおすすめします。

  1. macOS CatalinaでGEMやRuby環境のエラーが発生した場合は、こちらの返信 を参考にしてください。

解決した問題!

以上の過程を経て、Slack Bot の動作方法や iOS App Store のレビュー取得方法がよく理解できました。また、Ruby にも触れてみましたが、書きやすくてとても良いです!

PostZMediumToMarkdown によって Medium から変換されました。

GitHub で編集
この記事を改善
本記事は Medium で初公開
オリジナルを読む
この記事をシェア
リンクをコピー · SNS でシェア
ZhgChgLi
著者

ZhgChgLi

An iOS, web, and automation developer from Taiwan 🇹🇼 who also loves sharing, traveling, and writing.

コメント