appsendr 0.0.3 → 0.0.5
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.
- data/Manifest +4 -0
- data/Rakefile +2 -2
- data/appsendr.gemspec +5 -8
- data/lib/appsendr/client.rb +105 -29
- data/lib/appsendr/command.rb +14 -3
- data/lib/appsendr/commands/app.rb +46 -33
- data/lib/appsendr/commands/auth.rb +5 -21
- data/lib/appsendr/commands/base.rb +1 -0
- data/lib/appsendr/commands/build.rb +139 -19
- data/lib/appsendr/commands/collaborators.rb +29 -1
- data/lib/appsendr/commands/deploy.rb +13 -2
- data/lib/appsendr/commands/groups.rb +63 -0
- data/lib/appsendr/commands/help.rb +10 -3
- data/lib/appsendr/commands/portal.rb +375 -0
- data/lib/appsendr/commands/testers.rb +14 -31
- data/lib/appsendr/constants.rb +1 -1
- data/lib/appsendr/helpers.rb +31 -7
- data/lib/appsendr/progressbar.rb +236 -0
- metadata +12 -20
@@ -0,0 +1,375 @@
|
|
1
|
+
module AppSendr::Command
|
2
|
+
class Portal < Base
|
3
|
+
|
4
|
+
attr_accessor :credentials
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'mechanize'
|
8
|
+
|
9
|
+
@@agent = Mechanize.new
|
10
|
+
@@agent.user_agent_alias = 'Mac FireFox'
|
11
|
+
@@agent.follow_meta_refresh = true
|
12
|
+
|
13
|
+
@@available_devices = []
|
14
|
+
@@available_profiles = []
|
15
|
+
|
16
|
+
LOGIN_URL = 'https://developer.apple.com/ios/manage/overview/index.action'
|
17
|
+
DEVICES_URL = "https://developer.apple.com/ios/manage/devices/index.action"
|
18
|
+
DEVICE_BULK_UPLOAD_URL = "http://developer.apple.com/ios/manage/devices/saveupload.action"
|
19
|
+
SAVE_TEAM_URL = "http://developer.apple.com/membercenter/saveTeamSelection.action"
|
20
|
+
|
21
|
+
DEV_PROVISIONING_PROFILE_URL = "https://developer.apple.com/ios/manage/provisioningprofiles/index.action"
|
22
|
+
DIST_PROVISIONING_PROFILE_URL = "https://developer.apple.com/ios/manage/provisioningprofiles/viewDistributionProfiles.action"
|
23
|
+
|
24
|
+
CREATE_ADHOC_PROFILE_URL = "http://developer.apple.com/ios/manage/provisioningprofiles/create.action?type=2"
|
25
|
+
DOWNLOAD_PROFILE_URL_WO_ID ="/ios/manage/provisioningprofiles/download.action?blobId="
|
26
|
+
EDIT_PROFILE_URL_WO_ID = "/ios/manage/provisioningprofiles/edit.action?provDisplayId="
|
27
|
+
|
28
|
+
|
29
|
+
def index
|
30
|
+
|
31
|
+
result = check_for_manual_login
|
32
|
+
|
33
|
+
if result
|
34
|
+
@available_devices = get_devices
|
35
|
+
@available_dev_profiles = development_provisioning_profiles
|
36
|
+
@available_dist_profiles = distribution_provisioning_profiles
|
37
|
+
|
38
|
+
display "#{@available_devices.length} Devices"
|
39
|
+
display "#{@available_dev_profiles.length} Development Profiles"
|
40
|
+
display "#{@available_dist_profiles.length} Distribution Profiles"
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def devices
|
46
|
+
result = check_for_manual_login
|
47
|
+
|
48
|
+
if result
|
49
|
+
@available_devices = get_devices
|
50
|
+
@available_devices.each{|device|
|
51
|
+
display "#{device[:name]} - #{device[:id]}"
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def profiles
|
57
|
+
result = check_for_manual_login
|
58
|
+
|
59
|
+
if result
|
60
|
+
@available_dev_profiles = development_provisioning_profiles
|
61
|
+
@available_dist_profiles = distribution_provisioning_profiles
|
62
|
+
|
63
|
+
display "\nDevelopment Profiles\n"
|
64
|
+
@available_dev_profiles.each{|profile|
|
65
|
+
|
66
|
+
display "#{profile[:name]} - #{profile[:app_id]}"
|
67
|
+
}
|
68
|
+
display "\nDistribution Profiles\n"
|
69
|
+
@available_dist_profiles.each{|profile|
|
70
|
+
display "#{profile[:name]} - #{profile[:app_id]}"
|
71
|
+
}
|
72
|
+
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def update
|
78
|
+
|
79
|
+
@remote_devices = appsendr.devices(nil,false)
|
80
|
+
@available_devices = get_devices
|
81
|
+
|
82
|
+
## get devices for users for this app
|
83
|
+
## get all devices from portal
|
84
|
+
## reconcile
|
85
|
+
## add device not on portal
|
86
|
+
## get profile for app
|
87
|
+
##
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
private
|
92
|
+
def check_for_manual_login
|
93
|
+
username = extract_option('--username', false)
|
94
|
+
password = extract_option('--password', false)
|
95
|
+
|
96
|
+
result = false
|
97
|
+
if username and password
|
98
|
+
result = login(username,password)
|
99
|
+
|
100
|
+
error "\nAuthentication failed" unless result
|
101
|
+
|
102
|
+
else
|
103
|
+
result = login(user,pass)
|
104
|
+
end
|
105
|
+
|
106
|
+
return result
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
###
|
111
|
+
|
112
|
+
|
113
|
+
def login(username, password)
|
114
|
+
|
115
|
+
# Get login page
|
116
|
+
#puts "- fetching login page"
|
117
|
+
#page = agent.get('https://phobos.apple.com/WebObjects/MZLabel.woa/wa/default')
|
118
|
+
page = @@agent.get(LOGIN_URL)
|
119
|
+
|
120
|
+
|
121
|
+
# Submit form to login
|
122
|
+
login_form = page.forms.first
|
123
|
+
login_form['theAccountName']=username
|
124
|
+
login_form['theAccountPW']=password
|
125
|
+
page = @@agent.submit(login_form)
|
126
|
+
|
127
|
+
# Fail if it looks like we ended up back at a login page
|
128
|
+
if page.form('appleConnectForm')
|
129
|
+
#puts "Error: Login failed"
|
130
|
+
return nil
|
131
|
+
#exit
|
132
|
+
else
|
133
|
+
select_form = page.form('saveTeamSelection')
|
134
|
+
if select_form
|
135
|
+
options = []
|
136
|
+
select_form.fields.first.options.each{|option|
|
137
|
+
display "#{options.length + 1}. #{option.text}"
|
138
|
+
options.push([option.text,option.value])
|
139
|
+
|
140
|
+
}
|
141
|
+
print "Enter the # for the account you wish to login to: "
|
142
|
+
selection = ask
|
143
|
+
if selection.to_i > (options.length )
|
144
|
+
error "Invalid selection"
|
145
|
+
return nil
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
team = options[selection.to_i - 1]
|
150
|
+
select_form['memberDisplayId'] = team[1]
|
151
|
+
page = @@agent.submit(select_form, select_form.buttons[1])
|
152
|
+
return page
|
153
|
+
else
|
154
|
+
return page
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def get_details(username, password)
|
160
|
+
if login(username,password)
|
161
|
+
@available_devices = get_devices
|
162
|
+
@available_dev_profiles = development_provisioning_profiles
|
163
|
+
@available_dist_profiles = distribution_provisioning_profiles
|
164
|
+
display ""
|
165
|
+
|
166
|
+
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
#####
|
172
|
+
# provisioning
|
173
|
+
def get_devices
|
174
|
+
page = @@agent.get(DEVICES_URL)
|
175
|
+
table = page.at("//table/tbody") # This passes through to hpricot
|
176
|
+
devices = []
|
177
|
+
return devices unless table
|
178
|
+
table.children.each{|tr|
|
179
|
+
children = tr.children
|
180
|
+
device_name = children[2].content.strip
|
181
|
+
device_id = children[4].content.strip
|
182
|
+
devices.push({:name => device_name, :id => device_id })
|
183
|
+
}
|
184
|
+
|
185
|
+
return devices;
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
def upload_bulk_file
|
190
|
+
temp_file = TempFile.new('bulk_upload')
|
191
|
+
temp_file.write(device_file)
|
192
|
+
temp_file.close
|
193
|
+
@@agent.post(DEVICE_BULK_UPLOAD_URL, {:upload => temp_file})
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
|
198
|
+
#####
|
199
|
+
# provisioning
|
200
|
+
|
201
|
+
def provisioning_profiles(url)
|
202
|
+
page = @@agent.get(url)
|
203
|
+
table = page.at("//table/tbody")
|
204
|
+
profiles = []
|
205
|
+
return profiles unless table
|
206
|
+
|
207
|
+
table.children.each{|tr|
|
208
|
+
children = tr.children
|
209
|
+
cert_key = children[0].children[0]['value'].strip
|
210
|
+
cert_name = children[2].content.strip
|
211
|
+
app_id = children[4].content.strip
|
212
|
+
profiles.push({:name => cert_name, :id => cert_key, :app_id => app_id })
|
213
|
+
#p children
|
214
|
+
}
|
215
|
+
return profiles
|
216
|
+
end
|
217
|
+
|
218
|
+
def development_provisioning_profiles
|
219
|
+
provisioning_profiles(DEV_PROVISIONING_PROFILE_URL)
|
220
|
+
end
|
221
|
+
|
222
|
+
def distribution_provisioning_profiles
|
223
|
+
|
224
|
+
provisioning_profiles(DIST_PROVISIONING_PROFILE_URL)
|
225
|
+
|
226
|
+
end
|
227
|
+
|
228
|
+
|
229
|
+
def download_provisioning_profile(id)
|
230
|
+
page = @@agent.get(DOWNLOAD_PROFILE_URL_WO_ID+id)
|
231
|
+
return page
|
232
|
+
end
|
233
|
+
|
234
|
+
def edit_provisioning_profile(id)
|
235
|
+
page = @@agent.get(EDIT_PROFILE_URL_WO_ID+id)
|
236
|
+
end
|
237
|
+
|
238
|
+
def create_provisioning_profile
|
239
|
+
page = @@agent.get(CREATE_ADHOC_PROFILE_URL) #adhoc profiles
|
240
|
+
create_form = page.forms.first
|
241
|
+
|
242
|
+
end
|
243
|
+
|
244
|
+
## credentials
|
245
|
+
|
246
|
+
def user # :nodoc:
|
247
|
+
get_credentials
|
248
|
+
@credentials[0]
|
249
|
+
end
|
250
|
+
|
251
|
+
def pass # :nodoc:
|
252
|
+
get_credentials
|
253
|
+
@credentials[1]
|
254
|
+
end
|
255
|
+
|
256
|
+
|
257
|
+
def reauthorize
|
258
|
+
user_pass = ask_for_credentials
|
259
|
+
@credentials = auth_credentials(user_pass[0],user_pass[1])
|
260
|
+
write_credentials
|
261
|
+
end
|
262
|
+
|
263
|
+
def auth_credentials(username,password)
|
264
|
+
login_agent = Mechanize.new
|
265
|
+
|
266
|
+
page = login_agent.get(LOGIN_URL)
|
267
|
+
|
268
|
+
|
269
|
+
# Submit form to login
|
270
|
+
login_form = page.forms.first
|
271
|
+
login_form['theAccountName']=username
|
272
|
+
login_form['theAccountPW']=password
|
273
|
+
page = login_agent.submit(login_form);
|
274
|
+
# Fail if it looks like we ended up back at a login page
|
275
|
+
if (!page.form('appleConnectForm').nil?)
|
276
|
+
return false
|
277
|
+
else
|
278
|
+
return [username,password]
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def get_credentials # :nodoc:
|
283
|
+
return if @credentials
|
284
|
+
unless @credentials = read_credentials
|
285
|
+
user_pass = ask_for_credentials
|
286
|
+
|
287
|
+
@credentials = auth_credentials(user_pass[0],user_pass[1])
|
288
|
+
retry unless @credentials
|
289
|
+
save_credentials
|
290
|
+
end
|
291
|
+
@credentials
|
292
|
+
end
|
293
|
+
|
294
|
+
def read_credentials
|
295
|
+
File.exists?(credentials_file) and File.read(credentials_file).split("\n")
|
296
|
+
end
|
297
|
+
|
298
|
+
def echo_off
|
299
|
+
system "stty -echo"
|
300
|
+
end
|
301
|
+
|
302
|
+
def echo_on
|
303
|
+
system "stty echo"
|
304
|
+
end
|
305
|
+
|
306
|
+
def ask_for_credentials
|
307
|
+
puts "Enter your Apple Developer credentials. (These are never sent to AppSendr)"
|
308
|
+
|
309
|
+
print "Username: "
|
310
|
+
user = ask
|
311
|
+
|
312
|
+
print "Password: "
|
313
|
+
password = ask_for_password
|
314
|
+
|
315
|
+
[ user, password ]
|
316
|
+
end
|
317
|
+
|
318
|
+
def ask_for_password
|
319
|
+
echo_off
|
320
|
+
password = ask
|
321
|
+
puts
|
322
|
+
echo_on
|
323
|
+
return password
|
324
|
+
end
|
325
|
+
|
326
|
+
def save_credentials
|
327
|
+
begin
|
328
|
+
write_credentials
|
329
|
+
# if self.credentials
|
330
|
+
# delete_credentials
|
331
|
+
# raise e unless retry_login?
|
332
|
+
#
|
333
|
+
# display "\nAuthentication failed"
|
334
|
+
# user_pass = ask_for_credentials
|
335
|
+
# @credentials = auth_credentials(user_pass[0],user_pass[1])
|
336
|
+
# retry
|
337
|
+
rescue Exception => e
|
338
|
+
delete_credentials
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def retry_login?
|
343
|
+
@login_attempts ||= 0
|
344
|
+
@login_attempts += 1
|
345
|
+
@login_attempts < 3
|
346
|
+
end
|
347
|
+
|
348
|
+
def write_credentials
|
349
|
+
FileUtils.mkdir_p(File.dirname(credentials_file))
|
350
|
+
File.open(credentials_file, 'w') do |f|
|
351
|
+
f.puts self.credentials
|
352
|
+
end
|
353
|
+
set_credentials_permissions
|
354
|
+
end
|
355
|
+
|
356
|
+
def set_credentials_permissions
|
357
|
+
FileUtils.chmod 0700, File.dirname(credentials_file)
|
358
|
+
FileUtils.chmod 0600, credentials_file
|
359
|
+
end
|
360
|
+
|
361
|
+
def delete_credentials
|
362
|
+
FileUtils.rm_f(credentials_file)
|
363
|
+
end
|
364
|
+
|
365
|
+
def credentials_file
|
366
|
+
"#{home_directory}/#{AppSendr::APPDROPPR_DIR}/portal_credentials"
|
367
|
+
end
|
368
|
+
|
369
|
+
def credentials_setup?
|
370
|
+
File.exist?(credentials_file)
|
371
|
+
end
|
372
|
+
|
373
|
+
|
374
|
+
end
|
375
|
+
end
|
@@ -33,16 +33,15 @@ module AppSendr::Command
|
|
33
33
|
def add
|
34
34
|
|
35
35
|
if require_project(2,"add testers","an email and name", true)
|
36
|
-
f = testers_file_append
|
37
|
-
return unless f
|
38
36
|
email = args.shift.strip
|
39
37
|
name = args.join(" ").strip
|
40
|
-
|
41
|
-
f.close
|
38
|
+
|
42
39
|
begin
|
43
40
|
response = appsendr.add_tester(read_app_id,email,name)
|
41
|
+
message "#{email} added"
|
42
|
+
|
44
43
|
rescue RestClient::RequestFailed => e
|
45
|
-
|
44
|
+
message "Errors"
|
46
45
|
response = JSON.parse(e.http_body)
|
47
46
|
response['message'].each{|err|
|
48
47
|
display err.join(" ")
|
@@ -55,45 +54,29 @@ module AppSendr::Command
|
|
55
54
|
def remove
|
56
55
|
if require_project(1,"remove testers","an email")
|
57
56
|
entered_email = args.shift.strip
|
58
|
-
|
59
|
-
f = testers_file
|
60
|
-
return unless f
|
61
|
-
out = ""
|
62
|
-
found_email = nil
|
63
|
-
f.each do |line|
|
64
|
-
tester = line.split(',')
|
65
|
-
email = tester.first.strip
|
66
|
-
if email == entered_email
|
67
|
-
found_email = email
|
68
|
-
out << ""
|
69
|
-
else
|
70
|
-
out << line
|
71
|
-
end
|
72
|
-
end
|
73
|
-
f.pos = 0
|
74
|
-
f.print out
|
75
|
-
f.truncate(f.pos)
|
76
|
-
|
77
57
|
appsendr.remove_tester(read_app_id,entered_email)
|
58
|
+
message "Removed tester"
|
78
59
|
|
79
60
|
end
|
80
61
|
end
|
81
62
|
|
82
63
|
def clear
|
83
64
|
if require_project(0,"clear testers",nil)
|
84
|
-
|
85
|
-
return unless f
|
86
|
-
f.close
|
87
|
-
|
65
|
+
message "Removing all testers"
|
88
66
|
appsendr.add_tester(read_app_id)
|
89
67
|
|
90
68
|
end
|
91
69
|
end
|
92
70
|
|
93
71
|
def notify
|
94
|
-
|
95
|
-
|
96
|
-
|
72
|
+
if require_project(0,"clear testers",nil)
|
73
|
+
require_project_droppr
|
74
|
+
message "Notifying testers"
|
75
|
+
group = args.join(" ").strip
|
76
|
+
|
77
|
+
appsendr.notify(read_app_id,group)
|
78
|
+
end
|
79
|
+
|
97
80
|
|
98
81
|
end
|
99
82
|
|
data/lib/appsendr/constants.rb
CHANGED
data/lib/appsendr/helpers.rb
CHANGED
@@ -44,13 +44,40 @@ module AppSendr
|
|
44
44
|
File.exists?(project_appsendr_app) and File.read(project_appsendr_app).split("\n")
|
45
45
|
end
|
46
46
|
|
47
|
+
def error(msg)
|
48
|
+
STDERR.puts(msg)
|
49
|
+
exit 1
|
50
|
+
end
|
51
|
+
|
52
|
+
def message(msg)
|
53
|
+
puts("=== #{msg}")
|
54
|
+
end
|
55
|
+
|
56
|
+
### split
|
57
|
+
def is_file_to_large?(file)
|
58
|
+
in_mb = size_of_file(file).to_f
|
59
|
+
if in_mb > 10
|
60
|
+
return true
|
61
|
+
else
|
62
|
+
return false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def size_of_file(file)
|
67
|
+
size = File.size(file)
|
68
|
+
mb = 1024.0 * 1024.0
|
69
|
+
in_mb = size / mb
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
## droppr
|
47
74
|
def has_project_droppr?
|
48
75
|
return false unless File.directory?(project_appsendr)
|
49
76
|
return File.exist?(project_appsendr_app)
|
50
77
|
end
|
51
78
|
|
52
79
|
def require_project_droppr
|
53
|
-
error "Project directory doesn't exist. Have you created an app
|
80
|
+
error "Project directory doesn't exist. Have you created an this app on appsendr?" unless has_project_droppr?
|
54
81
|
end
|
55
82
|
|
56
83
|
def require_project_dir(action)
|
@@ -98,16 +125,13 @@ module AppSendr
|
|
98
125
|
else
|
99
126
|
require_project_dir(action)
|
100
127
|
error "This app is already linked to appsendr." unless !has_project_droppr?
|
101
|
-
|
102
128
|
end
|
103
129
|
|
104
130
|
return false
|
105
131
|
end
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
exit 1
|
110
|
-
end
|
132
|
+
|
133
|
+
|
134
|
+
|
111
135
|
end
|
112
136
|
end
|
113
137
|
|