記事

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

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

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

本記事は AI による翻訳をもとに作成されています。表現が不自然な箇所がありましたら、ぜひコメントでお知らせください。

記事一覧


AppStore APPのレビュー Slack ボットについて

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

Photo by [Austin Distel](https://unsplash.com/@austindistel?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText){:target="_blank"}

Photo by Austin Distel

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

[AppReviewBot を例に](https://appreviewbot.com){:target="_blank"}

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形式も利用可能です。

1
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
  "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は二段階認証を全面的に導入しているため、ログインセッションは定期的に更新する必要があります。

第一歩 — App Store Connect 管理画面のレビューセクションのデータ読み込みAPIを調査する:

Appleの管理画面にアクセスするには以下のコマンドを実行します:

1
https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/APP_ID/platforms/ios/reviews?index=0&sort=REVIEW_SORT_ORDER_MOST_RECENT

この endpoint はレビューリストを取得します:

index = ページネーションのオフセット。1回につき最大100件表示。

評価データの返却は以下の通りです:

private.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "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、ウェブ認証(二段階認証を含む)をすべて実装しています。私たちは Fastlane のコマンドを直接使用できます:

1
fastlane spaceauth -u <App Store Connect アカウント(メール)>

このコマンドはウェブログイン認証(2段階認証を含む)を完了し、cookie を FASTLANE_SESSION ファイルに保存します。

以下のような文字列が得られます:

1
2
3
4
5
6
7
8
9
10
11
12
!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

myacinfo = value を設定するだけで、レビューリストを取得できます。

ステップ3 — SpaceShip

本来は Fastlane はここまでしか使えないと思っていて、あとは自分で Fastlane から取得した cookie を使って API を叩くフローを組む必要があると思っていました。しかし、調査を進めるうちに、Fastlane の認証関連モジュール SpaceShip にはもっと強力な機能があることがわかりました!

`SpaceShip`

SpaceShip

SpaceShip には評価リストを取得するメソッド Class: Spaceship::TunesClient::get_reviews が既に用意されています!

1
2
app = Spaceship::Tunes::login(appstore_account, appstore_password)
reviews = app.get_reviews(app_id, platform, storefront, versionId = '')

*storefront = 地域

ステップ4 — 組み立て

Fastlane と Spaceship はどちらも Ruby で書かれているため、この Bot ツールも Ruby で作成します。

reviewBot.rb ファイルを作成し、実行するときはターミナルで以下を入力するだけです:

1
ruby reviewBot.rb

で問題ありません。( *ruby 環境の詳細な問題は記事末尾のヒントをご参照ください)

まず、元の get_reviews メソッドのパラメータは私たちの要件に合いませんでした。私が欲しいのは全地域・全バージョンのレビュー情報で、フィルタリング不要かつページネーション対応です:

extension.rb:

1
2
3
4
5
6
7
8
9
# 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 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 ? "作成日時: #{Time.at(review["lastModified"].to_i / 1000).to_datetime}" : "更新日時: #{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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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 ボット
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 ? "作成日時: #{Time.at(review["lastModified"].to_i / 1000).to_datetime}" : "更新日時: #{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"]} 通知送信成功!"
 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 やスケジューラーなどの定期実行ツールに追加するだけで完了です!

効果図:

無料の他の選択肢

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

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

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

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

ご注意ください

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

[#important-note-about-session-duration](https://docs.fastlane.tools/best-practices/continuous-integration/#important-note-about-session-duration){:target="_blank"}

#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 から変換されました。


🍺 Buy me a beer on PayPal

👉👉👉 Follow Me On Medium! (1,053+ Followers) 👈👈👈

本記事は Medium にて初公開されました(こちらからオリジナル版を確認)。ZMediumToMarkdown による自動変換・同期技術を使用しています。

Improve this page on Github.

本記事は著者により CC BY 4.0 に基づき公開されています。

© ZhgChgLi. All rights reserved.
閲覧数: 802,415+, 最終更新日時: 2026-01-15 11:14:58 +08:00

本サイトは Chirpy テーマを使用し、Jekyll 上で構築されています。
Medium の記事は ZMediumToMarkdown により変換されています。