ZReviewTender 1.3.7 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/bin/ZReviewTender +7 -7
- data/lib/AndroidFetcher.rb +1 -1
- data/lib/AppleFetcher.rb +1 -1
- data/lib/Helper.rb +1 -1
- data/lib/Models/Review.rb +3 -2
- data/lib/Models/ReviewFetcher.rb +2 -2
- data/lib/Processors/AsanaProcessor.rb +2 -0
- data/lib/Processors/SlackAndAsanaConnector.rb +159 -0
- data/lib/Processors/SlackProcessor.rb +19 -10
- metadata +6 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 767abeb4f548e38a266911a20c15a6307b756542db7317e444e2755c704e437c
|
4
|
+
data.tar.gz: 1d15d46a181b16e6ed3969e10e8925e139fe67b20437a6bd82eff34869e147cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b45ed4176179853c79686f0baa46ba70f8c76d8ed8f340403d27183f7435d9905fcac484965d01690c49005e7b9232ea62e971fc3ca96c9c317702cc508698af
|
7
|
+
data.tar.gz: 2835f394a70693c454bd4ce73acd161f7e636f9f62f7b78e340bbcc59197dc5ac21b58854abcc3f639fa60c60ff923b134d4ff81459dbfea0a0dd1272003564d
|
data/.version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.4.0
|
data/bin/ZReviewTender
CHANGED
@@ -91,12 +91,12 @@ class Main
|
|
91
91
|
appleDestExampleYML = "#{basePath}/#{defaultConfigDirName}/#{defaultAppleConfigFileName}"
|
92
92
|
androidDestExampleYML = "#{basePath}/#{defaultConfigDirName}/#{defaultAndroidConfigFileName}"
|
93
93
|
|
94
|
-
if !File.
|
94
|
+
if !File.exist?("#{basePath}/latestCheckTimestamp/.keep")
|
95
95
|
File.open("#{basePath}/latestCheckTimestamp/.keep", 'w') { |file| file.write("") }
|
96
96
|
end
|
97
97
|
#
|
98
|
-
if File.
|
99
|
-
if File.
|
98
|
+
if File.exist?(appleSourceExampleYML)
|
99
|
+
if File.exist?(appleDestExampleYML)
|
100
100
|
puts "Failed! Config YML File Exists: #{appleDestExampleYML}"
|
101
101
|
else
|
102
102
|
FileUtils.cp(appleSourceExampleYML, appleDestExampleYML)
|
@@ -104,8 +104,8 @@ class Main
|
|
104
104
|
end
|
105
105
|
end
|
106
106
|
#
|
107
|
-
if File.
|
108
|
-
if File.
|
107
|
+
if File.exist?(androidSourceExampleYML)
|
108
|
+
if File.exist?(androidDestExampleYML)
|
109
109
|
puts "Failed! Config YML File Exists: #{androidDestExampleYML}"
|
110
110
|
else
|
111
111
|
FileUtils.cp(androidSourceExampleYML, androidDestExampleYML)
|
@@ -129,7 +129,7 @@ class Main
|
|
129
129
|
opts.on('-d', '--delete', 'delete latest check timestamp log file.(factory reset)') do
|
130
130
|
FileUtils.rm_rf("#{basePath}/latestCheckTimestamp/")
|
131
131
|
Helper.createDirIfNotExist("#{basePath}/latestCheckTimestamp/")
|
132
|
-
if !File.
|
132
|
+
if !File.exist?("#{basePath}/latestCheckTimestamp/.keep")
|
133
133
|
File.open("#{basePath}/latestCheckTimestamp/.keep", 'w') { |file| file.write("") }
|
134
134
|
end
|
135
135
|
|
@@ -160,7 +160,7 @@ class Main
|
|
160
160
|
|
161
161
|
private
|
162
162
|
def configFileCheck(path, command)
|
163
|
-
if !File.
|
163
|
+
if !File.exist? path
|
164
164
|
puts "Make sure you have vaild config file at #{path}"
|
165
165
|
puts "Or use ZReviewTender #{command} specify config.yml file path"
|
166
166
|
raise "Config file not found: #{path}"
|
data/lib/AndroidFetcher.rb
CHANGED
@@ -112,7 +112,7 @@ class AndroidFetcher < ReviewFetcher
|
|
112
112
|
if !config.accountID.nil? && !config.appID.nil?
|
113
113
|
url = "https://play.google.com/console/developers/#{config.accountID}/app/#{config.appID}/user-feedback/review-details?reviewId=#{customerReviewID}"
|
114
114
|
end
|
115
|
-
reviews.append(Review.new(customerReviewPlatform, customerReviewID, customerReviewReviewerNickname, customerReviewRating, customerReviewTitle, customerReviewBody, customerReviewCreatedDateTimestamp, url, customerReviewVersionString, customerReviewTerritory))
|
115
|
+
reviews.append(Review.new(customerReviewPlatform, customerReviewID, customerReviewReviewerNickname, customerReviewRating, customerReviewTitle, customerReviewBody, customerReviewCreatedDateTimestamp, url, customerReviewVersionString, customerReviewTerritory, {}))
|
116
116
|
|
117
117
|
# init first time, need first review to set as latestCheckTimestamp
|
118
118
|
if latestCheckTimestamp == 0
|
data/lib/AppleFetcher.rb
CHANGED
@@ -80,7 +80,7 @@ class AppleFetcher < ReviewFetcher
|
|
80
80
|
break
|
81
81
|
else
|
82
82
|
url = "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/app/#{config.appID}/ios/ratingsResponses"
|
83
|
-
reviews.append(Review.new(nil, customerReviewID, customerReviewReviewerNickname, customerReviewRating, customerReviewTitle, customerReviewBody, customerReviewCreatedDateTimestamp, url, nil, customerReviewTerritory))
|
83
|
+
reviews.append(Review.new(nil, customerReviewID, customerReviewReviewerNickname, customerReviewRating, customerReviewTitle, customerReviewBody, customerReviewCreatedDateTimestamp, url, nil, customerReviewTerritory, {}))
|
84
84
|
|
85
85
|
# init first time, need first review to set as latestCheckTimestamp
|
86
86
|
if latestCheckTimestamp == 0
|
data/lib/Helper.rb
CHANGED
data/lib/Models/Review.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
2
|
|
3
3
|
class Review
|
4
|
-
attr_accessor :platform, :id, :userName, :rating, :title, :body, :createdDateTimestamp, :url, :appVersion, :territory
|
5
|
-
def initialize(platform, id, userName, rating, title, body, createdDateTimestamp, url, appVersion, territory)
|
4
|
+
attr_accessor :platform, :id, :userName, :rating, :title, :body, :createdDateTimestamp, :url, :appVersion, :territory, :tempData
|
5
|
+
def initialize(platform, id, userName, rating, title, body, createdDateTimestamp, url, appVersion, territory, tempData)
|
6
6
|
@platform = platform # 來源平台 android or apple
|
7
7
|
@id = id # review id
|
8
8
|
@userName = userName # reviewer
|
@@ -13,5 +13,6 @@ class Review
|
|
13
13
|
@url = url # 前往 review 的連結 (apple 不提供後台指定到某個 review 的連結)
|
14
14
|
@appVersion = appVersion # app 版本號
|
15
15
|
@territory = territory # apple: 地區(TWN/JPN...), android: 語系(zh-tw/en...)
|
16
|
+
@tempData = tempData # 程式內部傳遞使用
|
16
17
|
end
|
17
18
|
end
|
data/lib/Models/ReviewFetcher.rb
CHANGED
@@ -47,7 +47,7 @@ class ReviewFetcher
|
|
47
47
|
|
48
48
|
def isSentWelcomeMessage()
|
49
49
|
filePath = "#{config.baseExecutePath}/latestCheckTimestamp/#{platform}Welcome"
|
50
|
-
return File.
|
50
|
+
return File.exist?(filePath)
|
51
51
|
end
|
52
52
|
|
53
53
|
def setPlatformLatestCheckTimestamp(timestamp)
|
@@ -58,7 +58,7 @@ class ReviewFetcher
|
|
58
58
|
|
59
59
|
def getPlatformLatestCheckTimestamp()
|
60
60
|
filePath = "#{config.baseExecutePath}/latestCheckTimestamp/#{platform}"
|
61
|
-
if File.
|
61
|
+
if File.exist?(filePath)
|
62
62
|
return File.read(filePath).to_i
|
63
63
|
else
|
64
64
|
return 0
|
@@ -84,6 +84,8 @@ class AsanaProcessor < Processor
|
|
84
84
|
|
85
85
|
taskData = asanaAPI("/tasks", "POST", requestTaskData)
|
86
86
|
taskData = taskData["data"]
|
87
|
+
|
88
|
+
review.tempData["asanaTaskGID"] = taskData["gid"]
|
87
89
|
|
88
90
|
if !sectionID.nil? && !taskData.nil?
|
89
91
|
asanaAPI("/sections/#{sectionID}/addTask", "POST", {"task": taskData["gid"]})
|
@@ -0,0 +1,159 @@
|
|
1
|
+
$lib = File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Models/Review"
|
4
|
+
require "Models/Processor"
|
5
|
+
require "Helper"
|
6
|
+
require "pathname"
|
7
|
+
require "GoogleAPI"
|
8
|
+
|
9
|
+
class SlackAndAsanaConnector < Processor
|
10
|
+
|
11
|
+
attr_accessor :logger, :slackBotToken, :asanaToken, :projectID, :asanaAPIURL
|
12
|
+
|
13
|
+
def initialize(config, configFilePath, baseExecutePath)
|
14
|
+
@config = config
|
15
|
+
@configFilePath = configFilePath
|
16
|
+
@baseExecutePath = baseExecutePath
|
17
|
+
@logger = ZLogger.new(baseExecutePath)
|
18
|
+
|
19
|
+
@asanaAPIURL = "https://app.asana.com/api/1.0"
|
20
|
+
@asanaToken = Helper.unwrapRequiredParameter(config, "asanaToken")
|
21
|
+
@slackBotToken = Helper.unwrapRequiredParameter(config, "slackBotToken")
|
22
|
+
|
23
|
+
puts "[SlackAndAsanaConnector] Init Success."
|
24
|
+
end
|
25
|
+
|
26
|
+
def processReviews(reviews, platform)
|
27
|
+
if reviews.length < 1
|
28
|
+
return reviews
|
29
|
+
end
|
30
|
+
|
31
|
+
pendingReviews = reviews.dup
|
32
|
+
|
33
|
+
loop do
|
34
|
+
review = pendingReviews.shift
|
35
|
+
|
36
|
+
result = retrieveSlackMessage(review)
|
37
|
+
if !result["ok"]
|
38
|
+
if result["error"] == "ratelimited"
|
39
|
+
puts "[SlackAndAsanaConnector] Reached Rate Limited, sleep 1 sec..."
|
40
|
+
sleep(1)
|
41
|
+
pendingReviews.insert(0, review)
|
42
|
+
end
|
43
|
+
else
|
44
|
+
sendAsanaCommentMessage(review, "ref: #{result["permalink"]}")
|
45
|
+
end
|
46
|
+
|
47
|
+
puts "[SlackAndAsanaConnector] Connect Slack Message to Asana Task, rest: #{pendingReviews.length}"
|
48
|
+
break if pendingReviews.length < 1
|
49
|
+
end
|
50
|
+
|
51
|
+
pendingReviews = reviews.dup
|
52
|
+
|
53
|
+
loop do
|
54
|
+
review = pendingReviews.shift
|
55
|
+
asanaTaskURL = retrieveAsanaTaskURL(review)
|
56
|
+
if !asanaTaskURL.nil?
|
57
|
+
result = sendSlackCommnetMessage(review, "<#{asanaTaskURL}| Go To *Asana Task*>")
|
58
|
+
if !result["ok"]
|
59
|
+
if result["error"] == "ratelimited"
|
60
|
+
puts "[SlackAndAsanaConnector] Reached Rate Limited, sleep 1 sec..."
|
61
|
+
sleep(1)
|
62
|
+
pendingReviews.insert(0, review)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
puts "[SlackAndAsanaConnector] Connect Asana Task to Slack Message, rest: #{pendingReviews.length}"
|
68
|
+
break if pendingReviews.length < 1
|
69
|
+
end
|
70
|
+
|
71
|
+
return reviews
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
def retrieveSlackMessage(review)
|
76
|
+
uri = URI("https://slack.com/api/chat.getPermalink?channel=#{review.tempData["slackChannelID"]}&message_ts=#{review.tempData["slackTS"]}")
|
77
|
+
headers = {'Content-Type': 'application/json; charset=utf-8', 'Authorization': "Bearer #{slackBotToken}"}
|
78
|
+
|
79
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
80
|
+
http.use_ssl = true
|
81
|
+
req = Net::HTTP::Get.new(uri.request_uri, headers)
|
82
|
+
res = http.request(req)
|
83
|
+
|
84
|
+
result = JSON.parse(res.body)
|
85
|
+
return result
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
def sendSlackCommnetMessage(review, message)
|
90
|
+
payload = Payload.new()
|
91
|
+
payload.text = message
|
92
|
+
payload.channel = review.tempData["slackChannelID"]
|
93
|
+
payload.thread_ts = review.tempData["slackTS"]
|
94
|
+
payload.unfurl_links = true
|
95
|
+
payload.unfurl_media = true
|
96
|
+
uri = URI("https://slack.com/api/chat.postMessage")
|
97
|
+
headers = {'Content-Type': 'application/json; charset=utf-8', 'Authorization': "Bearer #{slackBotToken}"}
|
98
|
+
|
99
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
100
|
+
http.use_ssl = true
|
101
|
+
req = Net::HTTP::Post.new(uri.request_uri, headers)
|
102
|
+
req.body = payload.to_json
|
103
|
+
res = http.request(req)
|
104
|
+
|
105
|
+
result = JSON.parse(res.body)
|
106
|
+
return result
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
private
|
111
|
+
def retrieveAsanaTaskURL(review)
|
112
|
+
if review.tempData["asanaTaskGID"].nil? || review.tempData["asanaTaskGID"] == ""
|
113
|
+
return nil
|
114
|
+
end
|
115
|
+
|
116
|
+
return "https://app.asana.com/0/0/#{review.tempData["asanaTaskGID"]}/f"
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
def sendAsanaCommentMessage(review, message)
|
121
|
+
if review.tempData["asanaTaskGID"].nil? || review.tempData["asanaTaskGID"] == ""
|
122
|
+
return nil
|
123
|
+
end
|
124
|
+
|
125
|
+
path = "/tasks/#{review.tempData["asanaTaskGID"]}/stories"
|
126
|
+
uri = URI(asanaAPIURL+path)
|
127
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
128
|
+
https.use_ssl = true
|
129
|
+
|
130
|
+
request = Net::HTTP::Post.new(uri)
|
131
|
+
request['Content-Type'] = 'application/json'
|
132
|
+
request.body = JSON.dump({"data": {"text": message}})
|
133
|
+
|
134
|
+
request['Authorization'] = "Bearer #{asanaToken}"
|
135
|
+
|
136
|
+
response = https.request(request).read_body
|
137
|
+
|
138
|
+
return result = JSON.parse(response)
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
class Payload
|
143
|
+
attr_accessor :channel, :thread_ts, :text, :unfurl_links, :unfurl_media
|
144
|
+
|
145
|
+
def as_json(options={})
|
146
|
+
{
|
147
|
+
channel: @channel,
|
148
|
+
thread_ts: @thread_ts,
|
149
|
+
text: @text,
|
150
|
+
unfurl_links: @unfurl_links,
|
151
|
+
unfurl_media: @unfurl_media
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
def to_json(*options)
|
156
|
+
as_json(*options).to_json(*options)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -42,7 +42,7 @@ class SlackProcessor < Processor
|
|
42
42
|
return reviews
|
43
43
|
end
|
44
44
|
|
45
|
-
|
45
|
+
pendingRequests = []
|
46
46
|
|
47
47
|
# Slack Message Limit: posting one message per second per channel
|
48
48
|
reviews.each_slice(attachmentGroupByNumber) do |reviewGroup|
|
@@ -71,12 +71,16 @@ class SlackProcessor < Processor
|
|
71
71
|
payload.attachments.append(attachment)
|
72
72
|
end
|
73
73
|
|
74
|
-
|
74
|
+
pendingRequests.append({"payload" => payload, "reviewGroup" => reviewGroup})
|
75
75
|
end
|
76
|
+
|
77
|
+
resultReviews = []
|
76
78
|
|
77
79
|
loop do
|
78
|
-
|
79
|
-
|
80
|
+
pendingRequest = pendingRequests.shift
|
81
|
+
payload = pendingRequest["payload"]
|
82
|
+
reviewGroup = pendingRequest["reviewGroup"]
|
83
|
+
|
80
84
|
result = request(payload)
|
81
85
|
if !result[:ok]
|
82
86
|
logger.logError(payload)
|
@@ -84,12 +88,17 @@ class SlackProcessor < Processor
|
|
84
88
|
if result[:message] == "ratelimited"
|
85
89
|
puts "[SlackProcessor] Reached Rate Limited, sleep 1 sec..."
|
86
90
|
sleep(1)
|
87
|
-
|
91
|
+
pendingRequests.insert(0, pendingRequest)
|
92
|
+
end
|
93
|
+
elsif !result[:ts].nil?
|
94
|
+
reviewGroup.each do |review|
|
95
|
+
review.tempData["slackTS"] = result[:ts]
|
96
|
+
review.tempData["slackChannelID"] = targetChannel
|
88
97
|
end
|
89
98
|
end
|
90
99
|
|
91
|
-
puts "[SlackProcessor] Send new Review messages, rest: #{
|
92
|
-
break if
|
100
|
+
puts "[SlackProcessor] Send new Review messages, rest: #{pendingRequests.length}"
|
101
|
+
break if pendingRequests.length < 1
|
93
102
|
end
|
94
103
|
|
95
104
|
return reviews
|
@@ -140,10 +149,10 @@ class SlackProcessor < Processor
|
|
140
149
|
res = http.request(req)
|
141
150
|
|
142
151
|
if isInCommingWebHook
|
143
|
-
return {"ok":res.body == "ok", "message":nil}
|
152
|
+
return {"ok":res.body == "ok", "message":nil, "ts": res['ts']}
|
144
153
|
else
|
145
154
|
result = JSON.parse(res.body)
|
146
|
-
return {"ok":result["ok"] == true, "message":result['error']}
|
155
|
+
return {"ok":result["ok"] == true, "message":result['error'], "ts": result['ts']}
|
147
156
|
end
|
148
157
|
|
149
158
|
end
|
@@ -182,4 +191,4 @@ class SlackProcessor < Processor
|
|
182
191
|
as_json(*options).to_json(*options)
|
183
192
|
end
|
184
193
|
end
|
185
|
-
end
|
194
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ZReviewTender
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ZhgChgLi
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-03-22 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: net-http
|
@@ -30,16 +29,15 @@ dependencies:
|
|
30
29
|
requirements:
|
31
30
|
- - "~>"
|
32
31
|
- !ruby/object:Gem::Version
|
33
|
-
version: 2.
|
32
|
+
version: 2.10.0
|
34
33
|
type: :runtime
|
35
34
|
prerelease: false
|
36
35
|
version_requirements: !ruby/object:Gem::Requirement
|
37
36
|
requirements:
|
38
37
|
- - "~>"
|
39
38
|
- !ruby/object:Gem::Version
|
40
|
-
version: 2.
|
39
|
+
version: 2.10.0
|
41
40
|
description: ZReviewTender - App Reviews Automatic Bot
|
42
|
-
email:
|
43
41
|
executables:
|
44
42
|
- ZReviewTender
|
45
43
|
extensions: []
|
@@ -64,13 +62,13 @@ files:
|
|
64
62
|
- lib/Processors/GoogleSheetProcessor.rb
|
65
63
|
- lib/Processors/GoogleTranslateProcessor.rb
|
66
64
|
- lib/Processors/ProcessorTemplate.rb
|
65
|
+
- lib/Processors/SlackAndAsanaConnector.rb
|
67
66
|
- lib/Processors/SlackProcessor.rb
|
68
67
|
- lib/ZLogger.rb
|
69
68
|
homepage: https://github.com/ZhgChgLi/ZReviewTender
|
70
69
|
licenses:
|
71
70
|
- MIT
|
72
71
|
metadata: {}
|
73
|
-
post_install_message:
|
74
72
|
rdoc_options: []
|
75
73
|
require_paths:
|
76
74
|
- lib
|
@@ -85,8 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
83
|
- !ruby/object:Gem::Version
|
86
84
|
version: '0'
|
87
85
|
requirements: []
|
88
|
-
rubygems_version: 3.
|
89
|
-
signing_key:
|
86
|
+
rubygems_version: 3.6.2
|
90
87
|
specification_version: 4
|
91
88
|
summary: ZReviewTender uses brand new App Store & Google Play API to fetch App reviews
|
92
89
|
and integration your workflow.
|