AppStore 评价追踪 Slack Bot 教学|用 Ruby + Fastlane 自动通知最新评论
iOS 开发者面对 AppStore 评价难即时监控,透过 Ruby 与 Fastlane 自动化取得最新评论,搭配 Slack 通知,有效避免旧评价干扰,提升回应效率与用户体验。
Click here to view the English version of this article.
點擊這裡查看本文章正體中文版本。
基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。
AppStore APP’s Reviews Slack Bot 那些事
使用 Ruby+Fastlane-SpaceShip 动手打造 APP 评价追踪通知 Slack 机器人
Photo by Austin Distel
吃米不知米价
最近才知道 Slack 中转发 APP 最新评价讯息的机器人是要付费的,我一直以为这功能是免费的;费用从 $5 到 $200 美金/月都有,因为各平台都不会只做「App Review Bot」的功能,其他还有数据统计、纪录、统一后台、与竞品比较…等等,费用也是照各平台能提供的服务为标准;Review Bot 只是他们的一环,但我就只想用这个功能其他不需要,如果是这样付费蛮浪费的。
问题
本来是用免费开源的工具 TradeMe/ReviewMe 来做 Slack 通知,但这个工具已年久失修,时不时 Slack 会爆喷一些旧的评价,看得让人心惊胆颤(很多 Bug 都早已修复,害我们以为又有问题!),原因不明。
所以考虑找其他工具、方法取代。
TL;DR [2022/08/10] Update:
现已改用全新的 App Store Connect API 重新设计 App Reviews Bot,并更名重新推出「 ZReviewTender — 免费开源的 App Reviews 监控机器人 」。
====
2022/07/20 Update
App Store Connect API 现已支援 读取和管理 Customer Reviews ,App Store Connect API 原生已支援存取 App 评价, 不需要再使用 Fastlane — Spaceship 去后台拿评价。
原理探究
有了动机之后,再来研究下达成目标的原理。
官方 API ❌
苹果有提供 App Store Connect API ,但没提供捞取评价功能。
[2022/07/20 更新]: App Store Connect API 现已支援 读取和管理 Customer Reviews
Public URL API (RSS) ⚠️
苹果有提供公开的 APP 评价 RSS 订阅网址 ,而且除了 rss xml 还提供 json 格式。
1
https://itunes.apple.com/国家码/rss/customerreviews/id=APP_ID/page=1/sortBy=mostRecent/json
国家码:可参考 这份文件 。
APP_ID:前往 App 网页版,会得到网址: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"
}
}
优点:
公开、不需身份验证步骤即可存取
简单好用
缺点:
此 RSS API 很老旧都没更新
回传评价的资讯太少(没留言时间、已编辑过评价?、已回复?)
遇到资料错乱问题(后面几页偶尔会突然喷旧资料)
最多存取 10 页
关于我们遇到的最大问题是 3;但这部分不确定是我们用的 Bot 工具 问题,还是这个 RSS URL 资料有问题。
Private URL API ✅
这个方法说来有点旁门左道,也是我突发奇想发现的;但在后续参考了其他 Review Bot 做法之后发现很多网站也都是这样用,应该没什么问题而且我 4~5 年前就看过有工具这样做了,只是当时没深入研究。
优点:
同苹果后台资料
资料完整且最新
可做更多细节筛选
具备深度整合的 APP 工具也是用这个方法(AppRadar/AppReviewBot…)
缺点:
非官方公布方法(旁门左道)
因苹果实行全面两步骤登入,所以登入 session 需要定期更新。
第一步 — 嗅探 App Store Connect 后台评论区块 Load 资料的 API:
得到苹果后台是透过打:
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 = 分页 offset,一次最多显示 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 有了、要求的 header 知道了,再来就要想办法自动化取得后台这个 cookie 资讯。
第二步 —万能 Fastlane
因苹果现在实行全 Two-Step Verification,所以对于登入验证自动化变得更加烦琐,幸好与苹果斗智斗勇的 Fastlane ,除了正规的 App Store Connect API、iTMSTransporter、网页认证(包含两步骤认证)全都有实作;我们可以直接使用 Fastlane 的指令:
1
fastlane spaceauth -u <App Store Connect 帐号(Email)>
此指令会完成网页登入验证(包含两步骤认证),然后将 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
带入就能取得评价列表。
第三步 — SpaceShip
本来以为 Fastlane 只能帮我们到这了,再来要自己串起从 Fastlane 拿到 cookie 然后打 api 的 flow;没想到经过一番探索发现 Fastlane 关于验证这块的模组 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 = 地区
第四步 — 组装
Fastlane、Spaceship 都是由 ruby 撰写,所以我们也要用 ruby 来制作这个 Bot 小工具。
我们可以建立一个 reviewBot.rb
档案,编译执行时只需在 Terminal 输入:
1
ruby reviewBot.rb
即可。 ( *更多 ruby 环境问题可参考文末提示)
首先 ,因原本的 get_reviews 口的参数不符合我们需求;我想要的是全地区、全版本的评价资料、不需要筛选、支援分页:
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
所以我们自己在 TunesClient 中扩充一个方法,里面参数只带 app_id、platform = ios
( 全小写 )、index = 分页 offset。
再来组装登入验证、捞评价列表:
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 密码)
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 ? "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"]} send Notify Success!"
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'
# Config
$slack_web_hook = "目标通知的 web hook url"
$slack_debug_web_hook = "机器人有错误时的通知 web hook url"
$appstore_account = "APPStoreConnect 帐号(Email)"
$appstore_password = "APPStoreConnect 密码"
$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 = " (客服回复已过时)"
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 Error occurs!",
:text => error,
:footer => "*因苹果技术限制,精准评价爬取功能约每一个月需要重新登入设定,敬请见谅。"
}
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 通知我们回来检查(多半是 session 过期)。
最后只要将此脚本加到 crontab / schedule 等排程工具定时执行即可!
效果图:
免费的其他选择
AppFollow :使用 Public URL API (RSS),只能说堪用吧。
feedis.io :使用 Private URL API,需要把帐号密码给他们。
TradeMe/ReviewMe :自架服务(node.js),我们原先用这个,但遇到前述问题。
温馨提示
1.⚠️Private URL API 方法,如果用有二阶段验证的帐号,最长每 30 天都需要重新验证才能使用且目前无解;如果有办法生出没二阶段的帐号就可以无痛爽爽用。
#important-note-about-session-duration
2.⚠️不论是免费、付费、本文的自架;切勿使用开发者帐号,务必开一个独立的 App Store Connect 帐号使用,权限只开放「Customer Support」;防止资安问题。
3.Ruby 建议使用 rbenv 进行管理,因系统自带 2.6 版容易造成冲突。
4.在 macOS Catalina 如遇到 GEM、Ruby 环境错误问题,可参考 此回复 解决。
Problem Solved!
经过以上心路历程,更了解的 Slack Bot 的运作方式;还有 iOS App Store 是如何爬取评价内容的,另外也摸了下 ruby!写起来真不错!
有任何问题及指教欢迎 与我联络 。
本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。