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,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppStatusNotification
|
4
|
+
class ConnectAPI
|
5
|
+
module Client
|
6
|
+
module AppStoreVersion
|
7
|
+
def versions(query = {})
|
8
|
+
get('appStoreVersions', query)
|
9
|
+
end
|
10
|
+
|
11
|
+
def version(id, query = {})
|
12
|
+
get("appStoreVersions/#{id}", query)
|
13
|
+
end
|
14
|
+
|
15
|
+
def select_version_build(id, build_id:)
|
16
|
+
body = {
|
17
|
+
data: {
|
18
|
+
type: 'builds',
|
19
|
+
id: build_id
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
patch("appStoreVersions/#{id}/relationships/build", body: body)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppStatusNotification
|
4
|
+
class ConnectAPI
|
5
|
+
module Client
|
6
|
+
module Build
|
7
|
+
def app_latest_build(id)
|
8
|
+
app_builds(id, limit: 1).to_model
|
9
|
+
end
|
10
|
+
|
11
|
+
def app_builds(id, **kargs)
|
12
|
+
kargs = kargs.merge(filter: { app: id })
|
13
|
+
builds(**kargs)
|
14
|
+
end
|
15
|
+
|
16
|
+
def builds(limit: 200, sort: '-uploadedDate',
|
17
|
+
includes: ConnectAPI::Model::Build::ESSENTIAL_INCLUDES,
|
18
|
+
**kargs)
|
19
|
+
|
20
|
+
kargs = kargs.merge(limit: limit)
|
21
|
+
.merge(sort: sort)
|
22
|
+
.merge(include: includes)
|
23
|
+
|
24
|
+
get('builds', **kargs)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
module AppStatusNotification
|
6
|
+
class ConnectAPI
|
7
|
+
module Platform
|
8
|
+
IOS = 'IOS'
|
9
|
+
MAC_OS = 'MAC_OS'
|
10
|
+
TV_OS = 'TV_OS'
|
11
|
+
WATCH_OS = 'WATCH_OS'
|
12
|
+
|
13
|
+
ALL = [IOS, MAC_OS, TV_OS, WATCH_OS]
|
14
|
+
end
|
15
|
+
|
16
|
+
module ProcessStatus
|
17
|
+
PROCESSING = 'PROCESSING'
|
18
|
+
FAILED = 'FAILED'
|
19
|
+
INVALID = 'INVALID'
|
20
|
+
VALID = 'VALID'
|
21
|
+
end
|
22
|
+
|
23
|
+
module Model
|
24
|
+
def self.included(base)
|
25
|
+
Parser.types ||= []
|
26
|
+
Parser.types << base
|
27
|
+
base.extend(Parser)
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_accessor :id
|
31
|
+
attr_reader :attributes
|
32
|
+
attr_reader :rate
|
33
|
+
|
34
|
+
def initialize(id, attributes, rate)
|
35
|
+
@id = id
|
36
|
+
@attributes = attributes
|
37
|
+
@rate = rate
|
38
|
+
|
39
|
+
update_attributes(attributes)
|
40
|
+
end
|
41
|
+
|
42
|
+
def update_attributes(attributes)
|
43
|
+
(attributes || []).each do |key, value|
|
44
|
+
|
45
|
+
method = "#{key.to_s.underscore}=".to_sym
|
46
|
+
self.send(method, value) if self.respond_to?(method)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module Parser
|
52
|
+
class << self
|
53
|
+
attr_accessor :types
|
54
|
+
attr_accessor :types_cache
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.parse(response, rate)
|
58
|
+
body = response.body
|
59
|
+
data = body['data']
|
60
|
+
raise ConnectAPIError, 'No data' unless data
|
61
|
+
|
62
|
+
included = body['included'] || []
|
63
|
+
if data.kind_of?(Hash)
|
64
|
+
inflate_model(data, included, rate: rate)
|
65
|
+
elsif data.kind_of?(Array)
|
66
|
+
return data.map do |model_data|
|
67
|
+
inflate_model(model_data, included, rate: rate)
|
68
|
+
end
|
69
|
+
else
|
70
|
+
raise ConnectAPIError, "'data' is neither a hash nor an array"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.inflate_model(model_data, included, rate: {})
|
75
|
+
# Find class
|
76
|
+
type_class = find_class(model_data)
|
77
|
+
raise "No type class found for #{model_data['type']}" unless type_class
|
78
|
+
|
79
|
+
# Get id and attributes needed for inflating
|
80
|
+
id = model_data['id']
|
81
|
+
attributes = model_data['attributes']
|
82
|
+
|
83
|
+
# Instantiate object and inflate relationships
|
84
|
+
relationships = model_data['relationships'] || []
|
85
|
+
type_instance = type_class.new(id, attributes, rate)
|
86
|
+
|
87
|
+
inflate_model_relationships(type_instance, relationships, included)
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.find_class(model_data)
|
91
|
+
# Initialize cache
|
92
|
+
@types_cache ||= {}
|
93
|
+
|
94
|
+
# Find class in cache
|
95
|
+
type_string = model_data['type']
|
96
|
+
type_class = @types_cache[type_string]
|
97
|
+
return type_class if type_class
|
98
|
+
|
99
|
+
# Find class in array
|
100
|
+
type_class = @types.find do |type|
|
101
|
+
type.type == type_string
|
102
|
+
end
|
103
|
+
|
104
|
+
# Cache and return class
|
105
|
+
type_class
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.inflate_model_relationships(type_instance, relationships, included)
|
109
|
+
# Relationship attributes to set
|
110
|
+
attributes = {}
|
111
|
+
|
112
|
+
# 1. Iterate over relationships
|
113
|
+
# 2. Find id and type
|
114
|
+
# 3. Find matching id and type in included
|
115
|
+
# 4. Inflate matching data and set in attributes
|
116
|
+
relationships.each do |key, value|
|
117
|
+
# Validate data exists
|
118
|
+
value_data_or_datas = value['data']
|
119
|
+
next unless value_data_or_datas
|
120
|
+
|
121
|
+
# Map an included data object
|
122
|
+
map_data = lambda do |value_data|
|
123
|
+
id = value_data['id']
|
124
|
+
type = value_data['type']
|
125
|
+
|
126
|
+
relationship_data = included.find do |included_data|
|
127
|
+
id == included_data['id'] && type == included_data['type']
|
128
|
+
end
|
129
|
+
|
130
|
+
inflate_model(relationship_data, included) if relationship_data
|
131
|
+
end
|
132
|
+
|
133
|
+
# Map a hash or an array of data
|
134
|
+
if value_data_or_datas.kind_of?(Hash)
|
135
|
+
attributes[key] = map_data.call(value_data_or_datas)
|
136
|
+
elsif value_data_or_datas.kind_of?(Array)
|
137
|
+
attributes[key] = value_data_or_datas.map(&map_data)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
type_instance.update_attributes(attributes)
|
142
|
+
type_instance
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
require 'app_status_notification/connect_api/models/app'
|
149
|
+
require 'app_status_notification/connect_api/models/app_store_version'
|
150
|
+
require 'app_status_notification/connect_api/models/build'
|
151
|
+
require 'app_status_notification/connect_api/models/pre_release_version'
|
152
|
+
require 'app_status_notification/connect_api/models/app_store_version_submission'
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'app_status_notification/connect_api/model'
|
4
|
+
|
5
|
+
module AppStatusNotification::ConnectAPI::Model
|
6
|
+
class App
|
7
|
+
include AppStatusNotification::ConnectAPI::Model
|
8
|
+
|
9
|
+
attr_accessor :name
|
10
|
+
attr_accessor :bundle_id
|
11
|
+
attr_accessor :sku
|
12
|
+
attr_accessor :primary_locale
|
13
|
+
attr_accessor :is_opted_in_to_distribute_ios_app_on_mac_app_store
|
14
|
+
attr_accessor :removed
|
15
|
+
attr_accessor :is_aag
|
16
|
+
attr_accessor :available_in_new_territories
|
17
|
+
attr_accessor :content_rights_declaration
|
18
|
+
|
19
|
+
# include
|
20
|
+
attr_accessor :app_store_versions
|
21
|
+
attr_accessor :builds
|
22
|
+
|
23
|
+
def self.type
|
24
|
+
'apps'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'app_status_notification/connect_api/model'
|
4
|
+
|
5
|
+
module AppStatusNotification::ConnectAPI::Model
|
6
|
+
class AppStoreVersion
|
7
|
+
include AppStatusNotification::ConnectAPI::Model
|
8
|
+
|
9
|
+
attr_accessor :platform
|
10
|
+
attr_accessor :version_string
|
11
|
+
attr_accessor :app_store_state
|
12
|
+
attr_accessor :release_type
|
13
|
+
attr_accessor :earliest_release_date # 2020-06-17T12:00:00-07:00
|
14
|
+
attr_accessor :uses_idfa
|
15
|
+
attr_accessor :downloadable
|
16
|
+
attr_accessor :created_date
|
17
|
+
attr_accessor :version
|
18
|
+
attr_accessor :uploaded_date
|
19
|
+
attr_accessor :expiration_date
|
20
|
+
attr_accessor :expired
|
21
|
+
attr_accessor :store_icon
|
22
|
+
attr_accessor :watch_store_icon
|
23
|
+
attr_accessor :copyright
|
24
|
+
attr_accessor :min_os_version
|
25
|
+
|
26
|
+
# include
|
27
|
+
attr_accessor :app
|
28
|
+
attr_accessor :app_store_version_submission
|
29
|
+
attr_accessor :build
|
30
|
+
|
31
|
+
ESSENTIAL_INCLUDES = [
|
32
|
+
'app',
|
33
|
+
'appStoreVersionSubmission',
|
34
|
+
'build'
|
35
|
+
].join(',')
|
36
|
+
|
37
|
+
module AppStoreState
|
38
|
+
READY_FOR_SALE = 'READY_FOR_SALE'
|
39
|
+
PROCESSING_FOR_APP_STORE = 'PROCESSING_FOR_APP_STORE'
|
40
|
+
PENDING_DEVELOPER_RELEASE = 'PENDING_DEVELOPER_RELEASE'
|
41
|
+
IN_REVIEW = 'IN_REVIEW'
|
42
|
+
WAITING_FOR_REVIEW = 'WAITING_FOR_REVIEW'
|
43
|
+
DEVELOPER_REJECTED = 'DEVELOPER_REJECTED'
|
44
|
+
REJECTED = 'REJECTED'
|
45
|
+
PREPARE_FOR_SUBMISSION = 'PREPARE_FOR_SUBMISSION'
|
46
|
+
METADATA_REJECTED = 'METADATA_REJECTED'
|
47
|
+
INVALID_BINARY = 'INVALID_BINARY'
|
48
|
+
end
|
49
|
+
|
50
|
+
module ReleaseType
|
51
|
+
AFTER_APPROVAL = 'AFTER_APPROVAL'
|
52
|
+
MANUAL = 'MANUAL'
|
53
|
+
SCHEDULED = 'SCHEDULED'
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.type
|
57
|
+
'appStoreVersions'
|
58
|
+
end
|
59
|
+
|
60
|
+
def on_sale?
|
61
|
+
app_store_state == AppStoreState::READY_FOR_SALE ||
|
62
|
+
app_store_state == AppStoreState::PROCESSING_FOR_APP_STORE
|
63
|
+
end
|
64
|
+
|
65
|
+
def in_review?
|
66
|
+
app_store_state == AppStoreState::IN_REVIEW
|
67
|
+
end
|
68
|
+
|
69
|
+
def preparing?
|
70
|
+
app_store_state == AppStoreState::PREPARE_FOR_SUBMISSION ||
|
71
|
+
app_store_state == AppStoreState::DEVELOPER_REJECTED
|
72
|
+
end
|
73
|
+
|
74
|
+
def rejected?
|
75
|
+
app_store_state == AppStoreState::REJECTED ||
|
76
|
+
app_store_state == AppStoreState::METADATA_REJECTED ||
|
77
|
+
app_store_state == AppStoreState::INVALID_BINARY
|
78
|
+
end
|
79
|
+
|
80
|
+
def editable?
|
81
|
+
preparing? || rejected?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'app_status_notification/connect_api/model'
|
4
|
+
|
5
|
+
module AppStatusNotification::ConnectAPI::Model
|
6
|
+
class AppStoreVersionSubmission
|
7
|
+
include AppStatusNotification::ConnectAPI::Model
|
8
|
+
|
9
|
+
attr_accessor :can_reject
|
10
|
+
|
11
|
+
def self.type
|
12
|
+
'appStoreVersionSubmissions'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'app_status_notification/connect_api/model'
|
4
|
+
|
5
|
+
module AppStatusNotification::ConnectAPI::Model
|
6
|
+
class Build
|
7
|
+
include AppStatusNotification::ConnectAPI::Model
|
8
|
+
|
9
|
+
attr_accessor :version
|
10
|
+
attr_accessor :uploaded_date
|
11
|
+
attr_accessor :expiration_date
|
12
|
+
attr_accessor :expired
|
13
|
+
attr_accessor :min_os_version
|
14
|
+
attr_accessor :icon_asset_token
|
15
|
+
attr_accessor :processing_state
|
16
|
+
attr_accessor :uses_non_exempt_encryption
|
17
|
+
|
18
|
+
attr_accessor :app
|
19
|
+
attr_accessor :beta_app_review_submission
|
20
|
+
attr_accessor :beta_build_metrics
|
21
|
+
attr_accessor :build_beta_detail
|
22
|
+
attr_accessor :pre_release_version
|
23
|
+
|
24
|
+
ESSENTIAL_INCLUDES = 'app,preReleaseVersion'
|
25
|
+
|
26
|
+
def self.type
|
27
|
+
'builds'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'app_status_notification/connect_api/model'
|
4
|
+
|
5
|
+
module AppStatusNotification::ConnectAPI::Model
|
6
|
+
class PreReleaseVersion
|
7
|
+
include AppStatusNotification::ConnectAPI::Model
|
8
|
+
|
9
|
+
attr_accessor :version
|
10
|
+
attr_accessor :platform
|
11
|
+
|
12
|
+
def self.type
|
13
|
+
return 'preReleaseVersions'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module AppStatusNotification
|
6
|
+
class ConnectAPI
|
7
|
+
class Response
|
8
|
+
include Enumerable
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
attr_reader :response, :connection
|
12
|
+
|
13
|
+
def initialize(response, connection)
|
14
|
+
@response = response
|
15
|
+
@connection = connection
|
16
|
+
end
|
17
|
+
|
18
|
+
def_delegators :@response, :status, :headers
|
19
|
+
|
20
|
+
def request_url
|
21
|
+
@response.env.url
|
22
|
+
end
|
23
|
+
|
24
|
+
def rate
|
25
|
+
return {} unless value = response.headers[:x_rate_limit]
|
26
|
+
|
27
|
+
value.split(';').inject({}) do |r, q|
|
28
|
+
k, v = q.split(':')
|
29
|
+
case k
|
30
|
+
when 'user-hour-lim'
|
31
|
+
r.merge!(limit: v)
|
32
|
+
when 'user-hour-rem'
|
33
|
+
r.merge!(remaining: v)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def next_link
|
39
|
+
return if response.nil?
|
40
|
+
|
41
|
+
links = response[:links] || {}
|
42
|
+
links[:next]
|
43
|
+
end
|
44
|
+
|
45
|
+
# def all_pages
|
46
|
+
# next_pages(count: 0)
|
47
|
+
# end
|
48
|
+
|
49
|
+
# def next_pages(count: 1)
|
50
|
+
# count = count.to_i
|
51
|
+
# count = 0 if count <= 0
|
52
|
+
|
53
|
+
# responses = [self]
|
54
|
+
# counter = 0
|
55
|
+
|
56
|
+
# resp = self
|
57
|
+
# loop do
|
58
|
+
# resp = resp.next_page
|
59
|
+
# break if resp.nil?
|
60
|
+
|
61
|
+
# responses << resp
|
62
|
+
# counter += 1
|
63
|
+
|
64
|
+
# break if counter >= count
|
65
|
+
# end
|
66
|
+
|
67
|
+
# responses
|
68
|
+
# end
|
69
|
+
|
70
|
+
# def next_url
|
71
|
+
# return if response.nil?
|
72
|
+
|
73
|
+
# links = response[:links] || {}
|
74
|
+
# links[:next]
|
75
|
+
# end
|
76
|
+
|
77
|
+
# def next_page
|
78
|
+
# return unless url = next_url
|
79
|
+
|
80
|
+
# Response.new(connection.get(url), connection)
|
81
|
+
# end
|
82
|
+
|
83
|
+
def to_model
|
84
|
+
to_models.first
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_models
|
88
|
+
return [] if response.nil?
|
89
|
+
|
90
|
+
model_or_models = ConnectAPI::Parser.parse(response, rate)
|
91
|
+
[model_or_models].flatten
|
92
|
+
end
|
93
|
+
|
94
|
+
def each(&block)
|
95
|
+
to_models.each do |model|
|
96
|
+
yield(model)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|