Cb0c68c33994
ℹ️ℹ️ℹ️ The following content is translated by OpenAI.
Click here to view the original Chinese version. | 點此查看本文中文版
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
title: "App Store App Reviews Bot: What You Need to Know"
author: "ZhgChgLi"
date: 2021-04-21T15:16:31.071+0000
last_modified_at: 2024-04-13T16:38:28.675+0000
categories: ["ZRealm Dev."]
tags: ["slackbot", "ios-app-development", "ruby", "fastlane", "automator"]
description: "Build a Slack bot to track app reviews"
image:
path: /assets/cb0c68c33994/1*BMCG3cu21W5MbODBbhI-sA.jpeg
render_with_liquid: false
---
### App Store App Reviews Slack Bot: What You Need to Know
Using Ruby + Fastlane - SpaceShip to create a Slack bot for tracking app reviews.
{:target="_blank"}](/assets/cb0c68c33994/1*BMCG3cu21W5MbODBbhI-sA.jpeg)
Photo by [Austin Distel](https://unsplash.com/@austindistel?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText){:target="_blank"}
#### Ignorance is Bliss
{:target="_blank"}](/assets/cb0c68c33994/1*Iv6qvrBfyv3bU1NK1hPVHg.png)
[Example of AppReviewBot](https://appreviewbot.com){:target="_blank"}
I recently discovered that the bot for forwarding the latest app reviews in Slack is a paid service. I always thought this feature was free. The costs range from $5 to $200 per month, as most platforms do not solely focus on the "App Review Bot" functionality; they also offer data analytics, record-keeping, centralized management, and competitor comparisons, among other services. The pricing reflects the range of services provided by each platform. However, I only want to use this specific feature, and paying for additional services feels wasteful.
### The Problem
Initially, I used the free and open-source tool [TradeMe/ReviewMe](https://github.com/TradeMe/ReviewMe){:target="_blank"} for Slack notifications. However, this tool has not been maintained for years, and it occasionally sends outdated reviews, which can be quite alarming (many bugs have already been fixed, leading us to believe there were new issues!). The reasons for this are unclear.
Thus, I considered looking for alternative tools or methods.
### TL;DR [2022/08/10] Update:
I have now redesigned the App Reviews Bot using the brand new [App Store Connect API](../f1365e51902c/) and rebranded it as [ZReviewTender — a free and open-source App Reviews monitoring bot](../e36e48bb9265/).
====
### 2022/07/20 Update
The [App Store Connect API now supports reading and managing Customer Reviews](../f1365e51902c/). The App Store Connect API natively supports accessing app reviews, **eliminating the need to use** Fastlane — Spaceship to retrieve reviews.
### Understanding the Principles
With motivation in place, let's explore the principles behind achieving our goal.
#### Official API ❌
Apple provides the [App Store Connect API](https://developer.apple.com/app-store-connect/api/){:target="_blank"}, but it does not include a feature for fetching reviews.
\[2022/07/20 Update\]: [The App Store Connect API now supports reading and managing Customer Reviews](../f1365e51902c/).
#### Public URL API (RSS) ⚠️
Apple offers a public RSS feed for app reviews [RSS subscription URL](https://rss.itunes.apple.com/zh-tw){:target="_blank"}, and it provides both RSS XML and JSON formats.
```plaintext
https://itunes.apple.com/COUNTRY_CODE/rss/customerreviews/id=APP_ID/page=1/sortBy=mostRecent/json
- COUNTRY_CODE: Refer to this document.
- APP_ID: Visit the app’s webpage to get the URL: https://apps.apple.com/tw/app/APP_NAME/id 12345678, where the number after “id” is the App ID (purely numeric).
- page: You can request pages 1-10; requests beyond that will not be fulfilled.
- sortBy: Use
mostRecent/json
to request the latest reviews in JSON format; you can change it tomostRecent/xml
for XML format.
Review data is returned as follows:
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": "Awesome!"
},
"content": {
"label": "Life is worth it!",
"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": "Application"
}
},
"im:voteCount": {
"label": "0"
}
}
Advantages:
- Publicly accessible without authentication steps.
- Simple and easy to use.
Disadvantages:
- This RSS API is outdated and has not been updated.
- The returned review information is minimal (no timestamp for comments, edited reviews, or responses).
- Occasionally encounters data corruption issues (later pages may suddenly return old data).
- Access is limited to a maximum of 10 pages.
The biggest issue we encountered was point 3; however, it’s unclear whether this is a problem with the Bot tool we used or if there are issues with the RSS URL data.
Private URL API ✅
This method is somewhat unconventional, and I stumbled upon it by chance. However, after referencing other Review Bot implementations, I found that many websites use this approach, so it should be fine. I had seen tools doing this 4-5 years ago, but I didn’t delve into it at the time.
Advantages:
- Data is the same as that in Apple’s backend.
- Data is complete and up-to-date.
- Allows for more detailed filtering.
- Deeply integrated app tools also use this method (AppRadar/AppReviewBot…).
Disadvantages:
- This is not an officially published method (unconventional).
- Due to Apple’s implementation of two-step verification, the login session needs to be updated regularly.
Step One — Sniffing the API that Loads Review Data in App Store Connect:
Apple’s backend can be accessed by calling:
1
https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/APP_ID/platforms/ios/reviews?index=0&sort=REVIEW_SORT_ORDER_MOST_RECENT
This endpoint retrieves the list of reviews:
index = pagination offset, with a maximum of 100 entries displayed at once.
Review data is returned as follows:
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": "Awesome!",
"review": "Life is worth it!",
"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
}
After testing, I found that simply including cookie: myacinfo=<Token>
allows us to spoof the request and retrieve data:
With the API established and the required headers known, the next step is to automate the retrieval of this cookie information.
Step Two — The Versatile Fastlane
Since Apple now implements full Two-Step Verification, automating the login verification process has become more cumbersome. Fortunately, the clever tool Fastlane has implemented not only the official App Store Connect API but also iTMSTransporter and web authentication (including two-step verification). We can directly use the Fastlane command:
1
fastlane spaceauth -u <App Store Connect Email>
This command completes the web login verification (including two-step verification) and stores the cookie in the FASTLANE_SESSION file.
You will receive a string similar to the following:
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
By using myacinfo = value
, we can retrieve the list of reviews.
Step Three — SpaceShip
I initially thought Fastlane could only help us up to this point, and that we would have to manually connect the flow from Fastlane to the API. However, after some exploration, I discovered that Fastlane’s authentication module, SpaceShip
, has even more powerful features!
SpaceShip
already includes a method for retrieving the review list: 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 = region
Step Four — Assembly
Both Fastlane and SpaceShip are written in Ruby, so we will also use Ruby to create this bot tool.
We can create a file named reviewBot.rb
, and when compiling and executing, simply enter the following in the Terminal:
1
ruby reviewBot.rb
(*For more Ruby environment issues, please refer to the tips at the end of this document.)
First, since the original get_reviews
method’s parameters do not meet our needs, I want to retrieve review data for all regions and versions without filtering and with pagination support:
extension.rb:
1
2
3
4
5
6
7
8
9
# Extension 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
So we extend the TunesClient with a method that only takes app_id
, platform = 'ios'
(in lowercase), and index = pagination offset
.
Next, we assemble the login verification and retrieve the review list:
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 Email, APPStoreConnect Password)
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
We use a while loop to iterate through all pages, terminating when we reach a page with no content.
Next, we need to add a record of the last review’s timestamp to only notify about the latest messages that haven’t been sent yet:
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 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
# Do not send notifications on the first run
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+")
We simply use a .lastModified
file to record the last execution time.
*No notifications are sent on the first run to avoid flooding.
Final Step: Assemble the notification message and send it to 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
# 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 = " (Response is outdated)"
end
edited = review["edited"] == false ? "" : ":memo: User updated review#{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"]} send Notify Success!"
end
SLACK_WEB_HOOK_URL
= Incoming WebHook URL
Final Result
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
require "Spaceship"
require 'json'
require 'date'
# Config
$slack_web_hook = "Target notification web hook URL"
$slack_debug_web_hook = "Web hook URL for bot error notifications"
$appstore_account = "App Store Connect Email"
$appstore_password = "App Store Connect Password"
$app_id = "APP_ID"
$platform = "ios"
# Extension 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 = " (Customer response is outdated)"
end
edited = review["edited"] == false ? "" : ":memo: User updated review#{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
Here’s the translated text in naturalistic English while keeping the original markdown image source:
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
if review["value"]["lastModified"] > lastModified && lastModified != 0
# No notification on first use
messages.append(review["value"])
else
breakWhile = false
break
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 Error Occurred!",
:text => error,
:footer => "*Due to Apple's technical limitations, the precise review scraping feature requires re-login and setup approximately every month. We appreciate your understanding.*"
}
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
Additionally, I’ve added a begin...rescue
(try…catch) block for error protection. If an error occurs, a Slack notification will be sent to alert us to check back (most likely due to session expiration).
Finally, just add this script to a crontab/scheduling tool for regular execution!
Effect Screenshot:
Other Free Options
- AppFollow: Uses Public URL API (RSS), it’s just barely usable.
- feedis.io: Uses Private URL API, requires you to provide your account credentials.
- TradeMe/ReviewMe: Self-hosted service (node.js), we initially used this but encountered the aforementioned issues.
- JonSnow: Self-hosted service (GO), supports one-click deployment to Heroku, author: @saiday
Friendly Reminders
- ⚠️ For the Private URL API method, if you use an account with two-factor authentication, you will need to re-verify every 30 days at most, and currently, there is no solution; if you can create an account without two-factor authentication, you can use it without any hassle.
#important-note-about-session-duration
⚠️ Whether free, paid, or self-hosted as mentioned in this article; do not use a developer account. Be sure to create a separate App Store Connect account with permissions limited to “Customer Support” to prevent security issues.
It is recommended to use rbenv for Ruby management, as the system comes with version 2.6, which can cause conflicts.
If you encounter GEM or Ruby environment errors on macOS Catalina, you can refer to this reply for a solution.
Problem Solved!
Through this journey, I gained a better understanding of how the Slack Bot operates and how the iOS App Store scrapes review content. I also got a taste of Ruby! It’s quite enjoyable to write!
If you have any questions or suggestions, feel free to contact me.
```
This article was first published on Medium ➡️ Click Here
Automatically converted and synchronized using ZMediumToMarkdown and Medium-to-jekyll-starter.