app_status_notification 0.9.0.beta1
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 +7 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +69 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +112 -0
- data/LICENSE +21 -0
- data/README.md +22 -0
- data/app_status_notification.gemspec +46 -0
- data/config/locales/en.yml +10 -0
- data/config/locales/zh.yml +54 -0
- data/config/notification.yml +30 -0
- data/exe/app_status_notification +5 -0
- data/lib/app_status_notification.rb +24 -0
- data/lib/app_status_notification/command.rb +64 -0
- data/lib/app_status_notification/config.rb +234 -0
- data/lib/app_status_notification/connect_api.rb +92 -0
- data/lib/app_status_notification/connect_api/auth.rb +44 -0
- data/lib/app_status_notification/connect_api/clients/app.rb +46 -0
- data/lib/app_status_notification/connect_api/clients/app_store_version.rb +28 -0
- data/lib/app_status_notification/connect_api/clients/build.rb +29 -0
- data/lib/app_status_notification/connect_api/model.rb +152 -0
- data/lib/app_status_notification/connect_api/models/app.rb +27 -0
- data/lib/app_status_notification/connect_api/models/app_store_version.rb +84 -0
- data/lib/app_status_notification/connect_api/models/app_store_version_submission.rb +15 -0
- data/lib/app_status_notification/connect_api/models/build.rb +30 -0
- data/lib/app_status_notification/connect_api/models/pre_release_version.rb +16 -0
- data/lib/app_status_notification/connect_api/response.rb +102 -0
- data/lib/app_status_notification/error.rb +38 -0
- data/lib/app_status_notification/helper.rb +16 -0
- data/lib/app_status_notification/notification.rb +57 -0
- data/lib/app_status_notification/notifications/adapter.rb +25 -0
- data/lib/app_status_notification/notifications/dingtalk.rb +59 -0
- data/lib/app_status_notification/notifications/slack.rb +62 -0
- data/lib/app_status_notification/notifications/wecom.rb +48 -0
- data/lib/app_status_notification/runner.rb +409 -0
- data/lib/app_status_notification/store.rb +68 -0
- data/lib/app_status_notification/version.rb +5 -0
- data/lib/app_status_notification/watchman.rb +42 -0
- metadata +283 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppStatusNotification
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class ConfigError < Error; end
|
7
|
+
class MissingAppsConfigError < ConfigError; end
|
8
|
+
class UnknownNotificationError < ConfigError; end
|
9
|
+
|
10
|
+
class ConnectAPIError < Error
|
11
|
+
class << self
|
12
|
+
def parse(response)
|
13
|
+
errors = response.body['errors']
|
14
|
+
case response.status
|
15
|
+
when 401
|
16
|
+
InvalidUserCredentialsError.from_errors(errors)
|
17
|
+
when 409
|
18
|
+
InvalidEntityError.from_errors(errors)
|
19
|
+
else
|
20
|
+
ConnectAPIError.from_errors(errors)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def from_errors(errors)
|
25
|
+
message = ["Check errors(#{errors.size}) from response:"]
|
26
|
+
errors.each_with_index do |error, i|
|
27
|
+
message << "#{i + 1} - [#{error['status']}] #{error['title']}: #{error['detail']} in #{error['source']}"
|
28
|
+
end
|
29
|
+
|
30
|
+
new(message)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class RateLimitExceededError < ConnectAPIError; end
|
36
|
+
class InvalidEntityError < ConnectAPIError; end
|
37
|
+
class InvalidUserCredentialsError < ConnectAPIError; end
|
38
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n'
|
4
|
+
|
5
|
+
module AppStatusNotification
|
6
|
+
module I18nHelper
|
7
|
+
def t(key = nil, **kargs)
|
8
|
+
key ||= kargs.delete(:key)
|
9
|
+
raise 'No found key of i18n' if key.to_s.empty?
|
10
|
+
|
11
|
+
return key unless I18n.exists?(key)
|
12
|
+
|
13
|
+
I18n.t(key.to_sym, **kargs)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppStatusNotification
|
4
|
+
module Notification
|
5
|
+
class << self
|
6
|
+
def register(adapter, type, *shortcuts)
|
7
|
+
adapters[type.to_sym] = adapter
|
8
|
+
|
9
|
+
shortcuts.each do |name|
|
10
|
+
aliases[name.to_sym] = type.to_sym
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](type)
|
15
|
+
adapters[normalize(type)] || raise(UnknownNotificationError, "Unknown notication: #{type}")
|
16
|
+
end
|
17
|
+
|
18
|
+
def send(message, options)
|
19
|
+
adapter = options.delete('type')
|
20
|
+
notification = Notification[adapter].new(options)
|
21
|
+
notification.send(message)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# :nodoc:
|
27
|
+
def normalize(type)
|
28
|
+
aliases.fetch(type, type.to_sym)
|
29
|
+
end
|
30
|
+
|
31
|
+
# :nodoc:
|
32
|
+
def adapters
|
33
|
+
@adapters ||= {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# :nodoc:
|
37
|
+
def aliases
|
38
|
+
@aliases ||= {}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Message
|
43
|
+
attr_accessor :app, :version, :build
|
44
|
+
|
45
|
+
def initialize(app, version, build)
|
46
|
+
@app = app
|
47
|
+
@version = version
|
48
|
+
@build = build
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
require 'app_status_notification/notifications/adapter'
|
55
|
+
require 'app_status_notification/notifications/wecom'
|
56
|
+
require 'app_status_notification/notifications/slack'
|
57
|
+
require 'app_status_notification/notifications/dingtalk'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
module AppStatusNotification
|
8
|
+
module Notification
|
9
|
+
class Adapter
|
10
|
+
include AppStatusNotification::I18nHelper
|
11
|
+
|
12
|
+
def initialize(options = {}) # rubocop:disable Style/OptionHash
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
# def send(message)
|
17
|
+
# fail Error, 'Adapter does not supports #send'
|
18
|
+
# end
|
19
|
+
|
20
|
+
# def on_error(exception)
|
21
|
+
# fail Error, "Adapter does not supports #send"
|
22
|
+
# end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Doc: https://ding-doc.dingtalk.com/doc?spm=a1zb9.8233112.0.0.340c3a88sgMlJJ#/serverapi2/qf2nxq/9e91d73c
|
4
|
+
|
5
|
+
module AppStatusNotification
|
6
|
+
module Notification
|
7
|
+
class Dingtalk < Adapter
|
8
|
+
def initialize(options)
|
9
|
+
@webhook_url = URI(options['webhook_url'])
|
10
|
+
@secret = options['secret']
|
11
|
+
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def send(message)
|
16
|
+
message = t(**message)
|
17
|
+
|
18
|
+
data = {
|
19
|
+
msgtype: :markdown,
|
20
|
+
markdown: {
|
21
|
+
title: message,
|
22
|
+
text: message
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
response = Net::HTTP.post(build_url, data.to_json, 'Content-Type' => 'application/json')
|
27
|
+
ap response.code
|
28
|
+
ap response.body
|
29
|
+
# rescue => e
|
30
|
+
# @exception = e
|
31
|
+
# nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def build_url
|
35
|
+
url = @webhook_url.dup
|
36
|
+
query = url.query
|
37
|
+
if secret_sign = generate_sign
|
38
|
+
query = CGI.parse(query).merge(secret_sign)
|
39
|
+
end
|
40
|
+
url.query = URI.encode_www_form(query)
|
41
|
+
url
|
42
|
+
end
|
43
|
+
|
44
|
+
def generate_sign
|
45
|
+
return unless @secret
|
46
|
+
|
47
|
+
timestamp = (Time.now.to_f * 1000).to_i
|
48
|
+
sign = Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), @secret, "#{timestamp}\n#{@secret}")).strip
|
49
|
+
|
50
|
+
{
|
51
|
+
timestamp: timestamp,
|
52
|
+
sign: sign
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
Notification.register self, :dingtalk, :dingding
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Doc: https://api.slack.com/messaging/webhooks and https://app.slack.com/block-kit-builder/
|
4
|
+
|
5
|
+
module AppStatusNotification
|
6
|
+
module Notification
|
7
|
+
class Slack < Adapter
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
@webhook_url = URI(options['webhook_url'])
|
11
|
+
@token = options['token']
|
12
|
+
@channel = options['channel']
|
13
|
+
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def send(message)
|
18
|
+
data = if message.is_a?(String)
|
19
|
+
{
|
20
|
+
type: :mrkdwn,
|
21
|
+
text: message
|
22
|
+
}
|
23
|
+
else
|
24
|
+
{
|
25
|
+
blocks: [
|
26
|
+
{
|
27
|
+
type: :section,
|
28
|
+
text: {
|
29
|
+
type: 'mrkdwn',
|
30
|
+
text: t(**message),
|
31
|
+
}
|
32
|
+
},
|
33
|
+
{
|
34
|
+
type: :section,
|
35
|
+
fields: [
|
36
|
+
{
|
37
|
+
type: :mrkdwn,
|
38
|
+
text: "*版本:*\n#{message[:version]}"
|
39
|
+
},
|
40
|
+
{
|
41
|
+
type: :mrkdwn,
|
42
|
+
text: "*状态:*\n#{message[:status]}"
|
43
|
+
}
|
44
|
+
]
|
45
|
+
},
|
46
|
+
]
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
response = Net::HTTP.post(@webhook_url, data.to_json, 'Content-Type' => 'application/json')
|
51
|
+
|
52
|
+
ap response.code
|
53
|
+
ap response.body
|
54
|
+
# rescue => e
|
55
|
+
# @exception = e
|
56
|
+
# nil
|
57
|
+
end
|
58
|
+
|
59
|
+
Notification.register self, :slack
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Doc: https://work.weixin.qq.com/help?doc_id=13376 or https://work.weixin.qq.com/api/doc/90000/90136/91770
|
4
|
+
|
5
|
+
module AppStatusNotification
|
6
|
+
module Notification
|
7
|
+
class WeCom < Adapter
|
8
|
+
attr_reader :exception
|
9
|
+
|
10
|
+
MARKDOWN_MAX_LIMITED_LENGTH = 4096
|
11
|
+
|
12
|
+
@exception = nil
|
13
|
+
|
14
|
+
def initialize(**options)
|
15
|
+
@webhook_url = URI(options['webhook_url'])
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def send(message)
|
20
|
+
@exception = nil
|
21
|
+
|
22
|
+
message = t(message)
|
23
|
+
content = if message.bytesize >= MARKDOWN_MAX_LIMITED_LENGTH
|
24
|
+
"#{message[0..MARKDOWN_MAX_LIMITED_LENGTH-10]}\n\n..."
|
25
|
+
else
|
26
|
+
message
|
27
|
+
end
|
28
|
+
|
29
|
+
data = {
|
30
|
+
msgtype: :markdown,
|
31
|
+
markdown: {
|
32
|
+
content: content
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
response = Net::HTTP.post(@webhook_url, data.to_json, 'Content-Type' => 'application/json')
|
37
|
+
|
38
|
+
ap response.code
|
39
|
+
ap response.body
|
40
|
+
# rescue => e
|
41
|
+
# @exception = e
|
42
|
+
# nil
|
43
|
+
end
|
44
|
+
|
45
|
+
Notification.register self, :wecom, :wechat_work
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,409 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module AppStatusNotification
|
6
|
+
class Runner
|
7
|
+
include AppStatusNotification::I18nHelper
|
8
|
+
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
attr_reader :context
|
12
|
+
|
13
|
+
def initialize(context)
|
14
|
+
@context = context
|
15
|
+
determine_notification!
|
16
|
+
end
|
17
|
+
|
18
|
+
def_delegators :@context, :config, :client
|
19
|
+
def_delegators :config, :logger
|
20
|
+
|
21
|
+
def start
|
22
|
+
find_app
|
23
|
+
start_work
|
24
|
+
rescue Interrupt
|
25
|
+
logger.info t('logger.interrupt')
|
26
|
+
exit
|
27
|
+
rescue => e
|
28
|
+
Raven.capture_exception(e) unless config.dry?
|
29
|
+
|
30
|
+
logger.error t('logger.raise_error', message: e.full_message)
|
31
|
+
wait_next_loop
|
32
|
+
retry
|
33
|
+
end
|
34
|
+
|
35
|
+
def start_work
|
36
|
+
runloop do
|
37
|
+
edit_version = get_edit_version
|
38
|
+
next if not_found_edit_version(edit_version)
|
39
|
+
|
40
|
+
# 预检查提交版本是否真的存在
|
41
|
+
review_version = edit_version.version_string
|
42
|
+
next unless review_version
|
43
|
+
|
44
|
+
review_status = edit_version.app_store_state
|
45
|
+
check_app_store_version_changes(review_version, review_status)
|
46
|
+
app_store_status_changes_notification(edit_version)
|
47
|
+
|
48
|
+
next unless edit_version.editable?
|
49
|
+
|
50
|
+
check_selected_build_changes(edit_version)
|
51
|
+
build_processing_changes_notification(edit_version)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# 检查之前选中但被网页上人工取消该版本就发出通知
|
58
|
+
def check_selected_build_changes(edit_version)
|
59
|
+
cached_selected_build = store.selected_build
|
60
|
+
selected_build = edit_version.build
|
61
|
+
return unless cached_selected_build && !selected_build
|
62
|
+
|
63
|
+
store.unselected_build = cached_selected_build
|
64
|
+
store.delete :selected_build
|
65
|
+
send_notifications(
|
66
|
+
key: 'messages.app_build_removed',
|
67
|
+
app: app.name,
|
68
|
+
version: edit_version.version_string,
|
69
|
+
build: cached_selected_build
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
# 检查构建版本状态后(成功、失败)通知
|
74
|
+
def build_processing_changes_notification(edit_version)
|
75
|
+
# 获取最新上传的构建版本
|
76
|
+
|
77
|
+
latest_build = get_app_latest_build
|
78
|
+
return if latest_build.nil?
|
79
|
+
|
80
|
+
# 检查 build 的 app 版本是否和当前审核版本一致
|
81
|
+
return if latest_build.pre_release_version.version != edit_version.version_string
|
82
|
+
|
83
|
+
# 检查选中版本是否是最新版本
|
84
|
+
return if same_selected_build?(edit_version.build, latest_build)
|
85
|
+
|
86
|
+
review_version = edit_version.version_string
|
87
|
+
unless cached_latest_build?(review_version, latest_build)
|
88
|
+
case latest_build.processing_state
|
89
|
+
when ConnectAPI::ProcessStatus::PROCESSING
|
90
|
+
build_received_notification(review_version, latest_build)
|
91
|
+
when ConnectAPI::ProcessStatus::VALID
|
92
|
+
build_processed_notification(review_version, latest_build)
|
93
|
+
else
|
94
|
+
build_failed_notification(review_version, latest_build)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
selected_build_notification(edit_version, latest_build)
|
99
|
+
end
|
100
|
+
|
101
|
+
def same_selected_build?(selected_build, latest_build)
|
102
|
+
return false unless selected_build
|
103
|
+
|
104
|
+
is_same = selected_build.version == latest_build.version
|
105
|
+
if is_same && !store.selected_build
|
106
|
+
store.selected_build = selected_build.version
|
107
|
+
store.delete :unselected_build
|
108
|
+
|
109
|
+
send_notifications(
|
110
|
+
key: 'messages.select_appstoreversion_build_from_another_source',
|
111
|
+
app: app.name,
|
112
|
+
version: store.version,
|
113
|
+
build: selected_build.version
|
114
|
+
)
|
115
|
+
end
|
116
|
+
|
117
|
+
is_same
|
118
|
+
end
|
119
|
+
|
120
|
+
# 处理选中构建版本
|
121
|
+
def selected_build_notification(edit_version, latest_build)
|
122
|
+
cached_selected_build = store.selected_build
|
123
|
+
selected_build = edit_version.build
|
124
|
+
|
125
|
+
# 没有缓存和已经选中构建版本,尝试选中最新上传版本
|
126
|
+
if cached_selected_build.nil? && selected_build.nil?
|
127
|
+
return select_version_build(edit_version, latest_build)
|
128
|
+
end
|
129
|
+
|
130
|
+
# 发现选中版本写入缓存并发通知
|
131
|
+
if selected_build && !cached_selected_build
|
132
|
+
store.selected_build = selected_build.version
|
133
|
+
return send_notifications(
|
134
|
+
key: 'messages.app_build_processed',
|
135
|
+
app: app.name,
|
136
|
+
version: release_version,
|
137
|
+
build: edit_version.version
|
138
|
+
)
|
139
|
+
end
|
140
|
+
|
141
|
+
# 没有选中版本可能是网页上被删除选中
|
142
|
+
return unless selected_build
|
143
|
+
|
144
|
+
# 发现选择版本一样跳过
|
145
|
+
return if cached_selected_build == selected_build.version || selected_build.version == latest_build.version
|
146
|
+
|
147
|
+
# 选中构建版本和最新上传构建版本不一致通知
|
148
|
+
store.selected_build = selected_build.version
|
149
|
+
send_notifications(
|
150
|
+
key: 'messages.app_build_changed',
|
151
|
+
app: app.name,
|
152
|
+
version: release_version,
|
153
|
+
old_build: cached_selected_build,
|
154
|
+
new_build: selected_build.version
|
155
|
+
)
|
156
|
+
end
|
157
|
+
|
158
|
+
def select_version_build(edit_version, build)
|
159
|
+
# 如果曾经选中被移除不再重新选中
|
160
|
+
return if store.unselected_build == build.version
|
161
|
+
|
162
|
+
send_notifications(
|
163
|
+
key: 'messages.prepare_appstoreversion_build',
|
164
|
+
app: app.name,
|
165
|
+
version: edit_version.version_string,
|
166
|
+
build: build.version
|
167
|
+
)
|
168
|
+
|
169
|
+
r = client.select_version_build(edit_version.id, build_id: build.id)
|
170
|
+
if r.status == 204
|
171
|
+
store.selected_build = build.version
|
172
|
+
send_notifications(
|
173
|
+
key: 'messages.success_select_appstoreversion_build',
|
174
|
+
app: app.name,
|
175
|
+
version: edit_version.version_string,
|
176
|
+
build: build.version
|
177
|
+
)
|
178
|
+
else
|
179
|
+
send_notifications(
|
180
|
+
key: 'messages.failed_select_appstoreversion_build',
|
181
|
+
app: app.name,
|
182
|
+
version: edit_version.version_string,
|
183
|
+
build: build.version
|
184
|
+
)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def cached_latest_build?(version, build)
|
189
|
+
return true if store.version == version && store.latest_build == build.version
|
190
|
+
|
191
|
+
store.latest_build = build.version
|
192
|
+
false
|
193
|
+
end
|
194
|
+
|
195
|
+
# 没有找到新建版本的审核
|
196
|
+
def not_found_edit_version(edit_version)
|
197
|
+
return if edit_version
|
198
|
+
|
199
|
+
live_version = get_live_version
|
200
|
+
logger.debug t('logger.not_found_edit_version', version: live_version.version_string)
|
201
|
+
|
202
|
+
app_on_sale_with_uncatch_process_notification(live_version)
|
203
|
+
store.clear
|
204
|
+
|
205
|
+
true
|
206
|
+
end
|
207
|
+
|
208
|
+
def app_on_sale_with_uncatch_process_notification(live_version)
|
209
|
+
if (cache_version = store.version) &&
|
210
|
+
Gem::Version.new(live_version.version_string) >= Gem::Version.new(cache_version)
|
211
|
+
|
212
|
+
# TODO: 审核的版本已经发布发送通知
|
213
|
+
send_notifications(
|
214
|
+
key: 'app_was_on_sale',
|
215
|
+
app: app.name,
|
216
|
+
version: live_version.version_string
|
217
|
+
)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# 检查编辑版本号是否发生变化
|
222
|
+
def check_app_store_version_changes(version, status)
|
223
|
+
cached_version = store.version
|
224
|
+
store.version = version
|
225
|
+
|
226
|
+
status_text = t("app_store_status.#{status.downcase}")
|
227
|
+
if cached_version.to_s.empty?
|
228
|
+
store.status = status
|
229
|
+
|
230
|
+
send_notifications(
|
231
|
+
key: 'messages.app_version_created',
|
232
|
+
app: app.name,
|
233
|
+
version: version,
|
234
|
+
status: status_text
|
235
|
+
)
|
236
|
+
elsif cached_version != version
|
237
|
+
send_notifications(
|
238
|
+
key: 'messages.app_version_changed',
|
239
|
+
app: app.name,
|
240
|
+
current_version: cached_version,
|
241
|
+
new_version: version,
|
242
|
+
status: status_text
|
243
|
+
)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# 状态变更的通知
|
248
|
+
def app_store_status_changes_notification(edit_version)
|
249
|
+
status = edit_version.app_store_state
|
250
|
+
version = edit_version.version_string
|
251
|
+
return if status == store.status
|
252
|
+
|
253
|
+
logger.info "#{app.name} v#{version} changed status to `#{status}` created at #{edit_version.created_date}"
|
254
|
+
|
255
|
+
store.status = status
|
256
|
+
status_text = t("app_store_status.#{status.downcase}")
|
257
|
+
todo_text = t("todo.#{status.downcase}")
|
258
|
+
message = if I18n.exists?(:"todo.#{status.downcase}")
|
259
|
+
{
|
260
|
+
key: 'messages.app_store_status_changes_with_todo',
|
261
|
+
app: app.name,
|
262
|
+
version: version,
|
263
|
+
status: status_text,
|
264
|
+
todo: todo_text
|
265
|
+
}
|
266
|
+
else
|
267
|
+
{
|
268
|
+
key: 'messages.app_store_status_changes',
|
269
|
+
app: app.name,
|
270
|
+
version: version,
|
271
|
+
status: status_text
|
272
|
+
}
|
273
|
+
end
|
274
|
+
|
275
|
+
send_notifications(message)
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
#####################
|
280
|
+
# Notifcations
|
281
|
+
#####################
|
282
|
+
|
283
|
+
# 发出接收到构建版本通知
|
284
|
+
def build_received_notification(version, build)
|
285
|
+
send_notifications(
|
286
|
+
key: 'messages.app_build_received',
|
287
|
+
app: app.name,
|
288
|
+
version: version,
|
289
|
+
build: build.version
|
290
|
+
)
|
291
|
+
end
|
292
|
+
|
293
|
+
# 发出处理完毕构建版本通知
|
294
|
+
def build_processed_notification(version, build)
|
295
|
+
send_notifications(
|
296
|
+
key: 'messages.app_build_processed',
|
297
|
+
app: app.name,
|
298
|
+
version: version,
|
299
|
+
build: build.version
|
300
|
+
)
|
301
|
+
end
|
302
|
+
|
303
|
+
# 发出构建版本处理失败、无效通知
|
304
|
+
def build_failed_notification(version, build)
|
305
|
+
send_notifications(
|
306
|
+
key: 'messages.app_build_failed',
|
307
|
+
app: app.name,
|
308
|
+
version: version,
|
309
|
+
build: build.version,
|
310
|
+
status: build.processing_state
|
311
|
+
)
|
312
|
+
end
|
313
|
+
|
314
|
+
def send_notifications(message)
|
315
|
+
return unless message
|
316
|
+
|
317
|
+
allowed_notifications = context.app.notifications
|
318
|
+
|
319
|
+
config.notifications.each do |nname, nargs|
|
320
|
+
next unless allowed_notifications.size == 0 ||
|
321
|
+
allowed_notifications.include?(nname)
|
322
|
+
|
323
|
+
logger.debug t('logger.send_notification', name: nname, message: t(**message))
|
324
|
+
Notification.send(message, nargs) unless config.dry?
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
#####################
|
329
|
+
# App
|
330
|
+
#####################
|
331
|
+
|
332
|
+
# 获得当前 app 信息
|
333
|
+
def app
|
334
|
+
@app ||= client.app(context.app.id)
|
335
|
+
end
|
336
|
+
|
337
|
+
#####################
|
338
|
+
# Version
|
339
|
+
#####################
|
340
|
+
|
341
|
+
def get_edit_version
|
342
|
+
version = client.app_edit_version(app.id)
|
343
|
+
logger.debug "API Rate: #{version.rate}" if version
|
344
|
+
version
|
345
|
+
end
|
346
|
+
|
347
|
+
def get_live_version
|
348
|
+
client.app_live_version(app.id)
|
349
|
+
end
|
350
|
+
|
351
|
+
#####################
|
352
|
+
# Build
|
353
|
+
#####################
|
354
|
+
|
355
|
+
def get_app_latest_build
|
356
|
+
client.app_latest_build(app.id)
|
357
|
+
end
|
358
|
+
|
359
|
+
#####################
|
360
|
+
# Internal
|
361
|
+
#####################
|
362
|
+
|
363
|
+
def determine_notification!
|
364
|
+
global_notifications = config.notifications
|
365
|
+
app_notifications = context.app.notifications
|
366
|
+
enabled_notifications = app_notifications.empty? ? global_notifications.keys : app_notifications
|
367
|
+
logger.info "Enabled notifications (#{enabled_notifications.size}): #{enabled_notifications.join(', ')}"
|
368
|
+
end
|
369
|
+
|
370
|
+
def find_app
|
371
|
+
logger.info t('logger.found_app', name: app.name, id: app.id, bundle_id: app.bundle_id)
|
372
|
+
end
|
373
|
+
|
374
|
+
def store
|
375
|
+
@store ||= Store.new context.app.id, config.store_path
|
376
|
+
end
|
377
|
+
|
378
|
+
def runloop(&block)
|
379
|
+
loop do
|
380
|
+
logger.debug store.to_h
|
381
|
+
block.call
|
382
|
+
wait_next_loop
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
def wait_next_loop
|
387
|
+
logger.debug t('logger.wait_next_loop', interval: config.refresh_interval)
|
388
|
+
sleep config.refresh_interval
|
389
|
+
end
|
390
|
+
|
391
|
+
class Context
|
392
|
+
extend Forwardable
|
393
|
+
|
394
|
+
attr_reader :app, :config
|
395
|
+
|
396
|
+
def initialize(account, app, config)
|
397
|
+
@account = account
|
398
|
+
@app = app
|
399
|
+
@config = config
|
400
|
+
end
|
401
|
+
|
402
|
+
def client
|
403
|
+
@client ||= ConnectAPI.from_context(self)
|
404
|
+
end
|
405
|
+
|
406
|
+
def_delegators :@account, :issuer_id, :key_id, :private_key
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|