app_status_notification 0.9.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|