atlantispro 0.1.2 → 0.2.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/Atlantis.gemspec +3 -3
- data/Gemfile.lock +50 -0
- data/bin/{testflight → distribution} +1 -2
- data/lib/Atlantis/portal.rb +18 -9
- data/lib/Atlantis/portal/agent.rb +1 -197
- data/lib/Atlantis/portal/commands.rb +7 -5
- data/lib/Atlantis/portal/commands/devices.rb +4 -35
- data/lib/Atlantis/portal/commands/groups.rb +11 -0
- data/lib/Atlantis/portal/commands/login.rb +4 -4
- data/lib/Atlantis/portal/commands/logout.rb +4 -4
- data/lib/Atlantis/portal/commands/people.rb +5 -35
- data/lib/Atlantis/portal/crashlytics/crashlyticsservice.rb +315 -0
- data/lib/Atlantis/portal/helpers.rb +74 -1
- data/lib/Atlantis/portal/service.rb +46 -0
- data/lib/Atlantis/portal/testflight/testflightservice.rb +207 -0
- data/lib/Atlantis/version.rb +1 -1
- metadata +15 -10
- data/lib/Atlantis/portal/commands/invites.rb +0 -16
- data/lib/Atlantis/portal/commands/lists.rb +0 -36
- data/lib/Atlantis/portal/commands/teams.rb +0 -23
@@ -1,12 +1,12 @@
|
|
1
1
|
command :logout do |c|
|
2
|
-
c.syntax = '
|
3
|
-
c.summary = 'Remove account credentials'
|
2
|
+
c.syntax = 'distribution logout'
|
3
|
+
c.summary = 'Remove stored account credentials'
|
4
4
|
c.description = ''
|
5
5
|
|
6
6
|
c.action do |args, options|
|
7
|
-
say_error "You are not authenticated" and abort unless Security::InternetPassword.find(:server =>
|
7
|
+
say_error "You are not authenticated" and abort unless Security::InternetPassword.find(:server => service.host)
|
8
8
|
|
9
|
-
Security::InternetPassword.delete(:server =>
|
9
|
+
Security::InternetPassword.delete(:server => service.host)
|
10
10
|
|
11
11
|
say_ok "Account credentials removed"
|
12
12
|
end
|
@@ -1,43 +1,13 @@
|
|
1
1
|
command :'people' do |c|
|
2
|
-
c.syntax = '
|
3
|
-
c.summary = 'Lists
|
2
|
+
c.syntax = 'distribution people'
|
3
|
+
c.summary = 'Lists testers currently signed up to the service'
|
4
4
|
c.description = ''
|
5
5
|
|
6
6
|
c.action do |args, options|
|
7
7
|
|
8
|
-
|
8
|
+
people = try{service.list_people(arguments.group)}
|
9
|
+
people = people.values
|
9
10
|
|
10
|
-
|
11
|
-
distribution_list = 'all'
|
12
|
-
end
|
13
|
-
|
14
|
-
people = try{agent.list_people(distribution_list)}
|
15
|
-
|
16
|
-
if (agent.format == "csv")
|
17
|
-
csv_string = CSV.generate do |csv|
|
18
|
-
csv << ["ID", "User", "Email", "Devices"]
|
19
|
-
|
20
|
-
people.each do |person|
|
21
|
-
csv << [person.id, person.name, person.email, person.devices]
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
puts csv_string
|
26
|
-
|
27
|
-
else
|
28
|
-
title = "Listing people"
|
29
|
-
|
30
|
-
table = Terminal::Table.new :title => title do |t|
|
31
|
-
t << ["ID", "User", "Email", "Devices"]
|
32
|
-
t.add_separator
|
33
|
-
people.each do |person|
|
34
|
-
t << [person.id, person.name, person.email, person.devices]
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
#table.align_column 2, :center
|
39
|
-
|
40
|
-
puts table
|
41
|
-
end
|
11
|
+
output('Testers', ["ID", "Name", "Email"], people)
|
42
12
|
end
|
43
13
|
end
|
@@ -0,0 +1,315 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
require 'security'
|
3
|
+
require 'uri'
|
4
|
+
require 'json'
|
5
|
+
require 'logger'
|
6
|
+
require 'nokogiri'
|
7
|
+
|
8
|
+
include Atlantis::Portal
|
9
|
+
|
10
|
+
module Atlantis
|
11
|
+
module Portal
|
12
|
+
class CrashlyticsService < ::Service
|
13
|
+
attr_accessor :developer_token, :access_token, :csrf_token, :login_data, :team_id
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
super
|
17
|
+
|
18
|
+
self.host = "crashlytics.com"
|
19
|
+
end
|
20
|
+
|
21
|
+
def get(uri, parameters = [], referer = nil, headers = {})
|
22
|
+
uri = ::File.join("https://#{self.host}", uri) unless /^https?/ === uri
|
23
|
+
|
24
|
+
#puts "Requesting: #{uri}"
|
25
|
+
|
26
|
+
unless (self.developer_token.nil?)
|
27
|
+
headers['X-CRASHLYTICS-DEVELOPER-TOKEN'] = self.developer_token
|
28
|
+
end
|
29
|
+
|
30
|
+
unless (self.access_token.nil?)
|
31
|
+
headers['X-CRASHLYTICS-ACCESS-TOKEN'] = self.access_token
|
32
|
+
end
|
33
|
+
|
34
|
+
unless (self.csrf_token.nil?)
|
35
|
+
headers['X-CSRF-Token'] = self.csrf_token
|
36
|
+
end
|
37
|
+
|
38
|
+
headers['X-Requested-With'] = 'XMLHttpRequest'
|
39
|
+
|
40
|
+
3.times do
|
41
|
+
|
42
|
+
agent.get(uri, parameters, referer, headers)
|
43
|
+
|
44
|
+
return agent.page
|
45
|
+
end
|
46
|
+
|
47
|
+
raise NetworkError
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Handles login and CSRF tokens
|
52
|
+
#
|
53
|
+
|
54
|
+
def bootstrap
|
55
|
+
|
56
|
+
if (self.csrf_token.nil?)
|
57
|
+
csrf!
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# First need developer token
|
62
|
+
#
|
63
|
+
if (self.developer_token.nil?)
|
64
|
+
config!
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Need to login too
|
69
|
+
#
|
70
|
+
if (self.access_token.nil?)
|
71
|
+
login!
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def list_devices(distribution_list)
|
76
|
+
people = list_people(distribution_list)
|
77
|
+
|
78
|
+
people_list = []
|
79
|
+
|
80
|
+
people.each do |person|
|
81
|
+
people_list << person.id
|
82
|
+
end
|
83
|
+
|
84
|
+
agent.post('/dashboard/team/export/devices/', { "members" => people_list.join('|'), "csrfmiddlewaretoken" => agent.page.parser.css("[name='csrfmiddlewaretoken']")[0]['value'] } )
|
85
|
+
|
86
|
+
device_list = agent.page.body.split( /\r?\n/ )
|
87
|
+
|
88
|
+
# Remove first one
|
89
|
+
device_list.shift
|
90
|
+
|
91
|
+
devices = []
|
92
|
+
|
93
|
+
device_list.each do |dev|
|
94
|
+
#puts dev
|
95
|
+
|
96
|
+
device = Device.new
|
97
|
+
device.udid = dev.split(/\t/)[0]
|
98
|
+
device.name = dev.split(/\t/)[1]
|
99
|
+
|
100
|
+
devices << device
|
101
|
+
end
|
102
|
+
|
103
|
+
devices
|
104
|
+
end
|
105
|
+
|
106
|
+
def list_apps
|
107
|
+
bootstrap
|
108
|
+
|
109
|
+
page = get("/api/v2/organizations/#{self.team_id}/apps")
|
110
|
+
|
111
|
+
apps = JSON.parse(page.body)
|
112
|
+
|
113
|
+
return apps
|
114
|
+
end
|
115
|
+
|
116
|
+
def list_testers(app_id)
|
117
|
+
bootstrap
|
118
|
+
page = get("/api/v2/organizations/#{self.team_id}/apps/#{app_id}/beta_distribution/testers_in_organization?include_developers=true")
|
119
|
+
|
120
|
+
testers = JSON.parse(page.body)
|
121
|
+
|
122
|
+
return testers['testers']
|
123
|
+
end
|
124
|
+
|
125
|
+
def list_people(group)
|
126
|
+
bootstrap
|
127
|
+
|
128
|
+
apps = list_apps
|
129
|
+
|
130
|
+
all_testers = {}
|
131
|
+
|
132
|
+
apps.each do |app|
|
133
|
+
testers = list_testers (app['id'])
|
134
|
+
|
135
|
+
#
|
136
|
+
# For each tester go through it's devices and add them
|
137
|
+
#
|
138
|
+
|
139
|
+
testers.each do |tester|
|
140
|
+
|
141
|
+
#
|
142
|
+
# If tester is not yet in, create it
|
143
|
+
#
|
144
|
+
|
145
|
+
if all_testers[tester['id']].nil?
|
146
|
+
|
147
|
+
person = Person.new
|
148
|
+
person.id = tester['id']
|
149
|
+
person.name = tester['name']
|
150
|
+
person.email = tester['email']
|
151
|
+
person.groups = []
|
152
|
+
person.devices = {}
|
153
|
+
|
154
|
+
add_group = false
|
155
|
+
|
156
|
+
if tester['groups']
|
157
|
+
groups = tester['groups']
|
158
|
+
|
159
|
+
groups.each do |current_group|
|
160
|
+
|
161
|
+
person_group = Group.new
|
162
|
+
person_group.id = current_group['id']
|
163
|
+
person_group.name = current_group['name']
|
164
|
+
person_group.alias = current_group['alias']
|
165
|
+
|
166
|
+
person.groups << person_group
|
167
|
+
|
168
|
+
if person_group.name == group or person_group.alias == group
|
169
|
+
add_group = true
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
if (add_group == true or group.nil?) and !person.name.empty?
|
175
|
+
all_testers[person.id] = person
|
176
|
+
end
|
177
|
+
else
|
178
|
+
person = all_testers[tester['id']]
|
179
|
+
end
|
180
|
+
|
181
|
+
append_devices(person, tester['devices'])
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
return all_testers
|
186
|
+
end
|
187
|
+
|
188
|
+
def list_devices(group)
|
189
|
+
bootstrap
|
190
|
+
|
191
|
+
testers = list_people(group)
|
192
|
+
|
193
|
+
devices = {}
|
194
|
+
|
195
|
+
testers.each do |id, tester|
|
196
|
+
devices = devices.merge (tester.devices)
|
197
|
+
end
|
198
|
+
|
199
|
+
return devices.values
|
200
|
+
end
|
201
|
+
|
202
|
+
def list_groups
|
203
|
+
testers = list_people(nil)
|
204
|
+
|
205
|
+
groups = {}
|
206
|
+
|
207
|
+
testers.each do |id, tester|
|
208
|
+
tester.groups.each do |group|
|
209
|
+
groups[group.id] = group
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
return groups.values
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
def append_devices (person, devices)
|
219
|
+
|
220
|
+
if devices.nil?
|
221
|
+
return nil
|
222
|
+
end
|
223
|
+
|
224
|
+
devices.each do |device|
|
225
|
+
|
226
|
+
if person.devices[device['identifier']].nil?
|
227
|
+
current_device = Device.new
|
228
|
+
current_device.manufacturer = device['manufacturer']
|
229
|
+
current_device.model = device['model']
|
230
|
+
current_device.os_version = device['os_version']
|
231
|
+
current_device.identifier = device['identifier']
|
232
|
+
current_device.name = device['name']
|
233
|
+
current_device.platform = device['platform']
|
234
|
+
current_device.model_name = device['model_name']
|
235
|
+
|
236
|
+
person.devices[current_device.identifier] = current_device
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def csrf!
|
243
|
+
page = get('/login')
|
244
|
+
|
245
|
+
token = page.at('meta[name="csrf-token"]')[:content]
|
246
|
+
|
247
|
+
unless token.nil?
|
248
|
+
self.csrf_token = token
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def config!
|
253
|
+
page = get ('/api/v2/client_boot/config_data')
|
254
|
+
|
255
|
+
config_object = JSON.parse(page.body)
|
256
|
+
|
257
|
+
unless config_object['developer_token'].nil?
|
258
|
+
self.developer_token = config_object['developer_token']
|
259
|
+
else
|
260
|
+
raise UnsuccessfulAuthenticationError
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def login!
|
265
|
+
begin
|
266
|
+
|
267
|
+
session = agent.post('https://crashlytics.com/api/v2/session', { "email" => self.username, "password" => self.password }, { 'X-CRASHLYTICS-DEVELOPER-TOKEN' => self.developer_token, 'X-CSRF-Token' => self.csrf_token, 'X-Requested-With' => 'XMLHttpRequest' } )
|
268
|
+
|
269
|
+
rescue Mechanize::ResponseCodeError => ex
|
270
|
+
message = JSON.parse(ex.page.body)
|
271
|
+
|
272
|
+
unless message['message'].nil?
|
273
|
+
puts message['message']
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
self.login_data = JSON.parse(agent.page.body)
|
278
|
+
|
279
|
+
unless self.login_data['token'].nil?
|
280
|
+
self.access_token = self.login_data['token']
|
281
|
+
|
282
|
+
select_team!
|
283
|
+
else
|
284
|
+
raise UnsuccessfulAuthenticationError
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def select_team!
|
289
|
+
|
290
|
+
if self.login_data['organizations'].nil?
|
291
|
+
raise UnknownTeamError
|
292
|
+
end
|
293
|
+
|
294
|
+
teams = self.login_data['organizations']
|
295
|
+
|
296
|
+
teams.each do |team|
|
297
|
+
#puts team['name']
|
298
|
+
|
299
|
+
if team['alias'] == self.team or team['name'] == self.team
|
300
|
+
self.team_id = team['id']
|
301
|
+
|
302
|
+
break
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
if self.team_id.nil?
|
307
|
+
raise UnknownTeamError
|
308
|
+
end
|
309
|
+
|
310
|
+
#puts "SELECTED TEAM: #{self.team_id}"
|
311
|
+
end
|
312
|
+
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
@@ -16,8 +16,22 @@ end
|
|
16
16
|
module Atlantis
|
17
17
|
module Portal
|
18
18
|
module Helpers
|
19
|
+
|
20
|
+
def arguments
|
21
|
+
unless @arguments
|
22
|
+
@arguments = Atlantis::Portal::CommandArguments.new
|
23
|
+
end
|
24
|
+
|
25
|
+
@arguments
|
26
|
+
end
|
27
|
+
|
19
28
|
def agent
|
20
29
|
unless @agent
|
30
|
+
|
31
|
+
#
|
32
|
+
# Web connecting agent
|
33
|
+
#
|
34
|
+
|
21
35
|
@agent = Atlantis::Portal::Agent.new
|
22
36
|
|
23
37
|
@agent.instance_eval do
|
@@ -34,6 +48,16 @@ module Atlantis
|
|
34
48
|
@agent
|
35
49
|
end
|
36
50
|
|
51
|
+
def service
|
52
|
+
|
53
|
+
unless @service
|
54
|
+
@service = arguments.service.casecmp('crashlytics') == 0 ? Atlantis::Portal::CrashlyticsService.new : Atlantis::Portal::TestFlightService.new
|
55
|
+
@service.arguments = arguments
|
56
|
+
end
|
57
|
+
|
58
|
+
@service
|
59
|
+
end
|
60
|
+
|
37
61
|
def pluralize(n, singular, plural = nil)
|
38
62
|
n.to_i == 1 ? "1 #{singular}" : "#{n} #{plural || singular + 's'}"
|
39
63
|
end
|
@@ -44,10 +68,59 @@ module Atlantis
|
|
44
68
|
begin
|
45
69
|
yield
|
46
70
|
rescue UnsuccessfulAuthenticationError
|
47
|
-
say_error "Could not authenticate with
|
71
|
+
say_error "Could not authenticate with the service. Check that your username & password are correct, and that your membership is valid and all pending Terms of Service & agreements are accepted. If this problem continues, try logging into the service from a browser to see what's going on." and abort
|
72
|
+
rescue UnknownTeamError
|
73
|
+
say_error "Could not find the specified team. Check if your team parameter is correct."
|
48
74
|
end
|
49
75
|
end
|
50
76
|
|
77
|
+
def output (title, fields, data)
|
78
|
+
|
79
|
+
if arguments.format == "csv"
|
80
|
+
output_string = CSV.generate do |csv|
|
81
|
+
csv << fields
|
82
|
+
|
83
|
+
data.each do |row|
|
84
|
+
csv << array_for_object(fields, row)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
elsif arguments.format == "json"
|
89
|
+
objects = []
|
90
|
+
|
91
|
+
data.each do |row|
|
92
|
+
object = {}
|
93
|
+
|
94
|
+
fields.each do |field|
|
95
|
+
object[field.downcase] = row[field.downcase]
|
96
|
+
end
|
97
|
+
|
98
|
+
objects << object
|
99
|
+
end
|
100
|
+
|
101
|
+
output_string = JSON.generate(objects)
|
102
|
+
else
|
103
|
+
output_string = Terminal::Table.new :title => title do |t|
|
104
|
+
t << fields
|
105
|
+
t.add_separator
|
106
|
+
data.each do |row|
|
107
|
+
t << array_for_object(fields, row)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
puts output_string
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def array_for_object(fields, row)
|
117
|
+
properties = []
|
118
|
+
|
119
|
+
fields.each do |field|
|
120
|
+
properties << row[field.downcase]
|
121
|
+
end
|
122
|
+
|
123
|
+
return properties
|
51
124
|
end
|
52
125
|
end
|
53
126
|
end
|