Class: Spaceship::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/spaceship/ui.rb,
lib/spaceship/ui/select_team.rb,
lib/spaceship/client.rb

Defined Under Namespace

Classes: InvalidUserCredentialsError, UnexpectedResponse, UserInterface

Constant Summary collapse

PROTOCOL_VERSION =
"QH65B2"

Instance Attribute Summary collapse

Automatic Paging collapse

Login and Team Selection collapse

Apps collapse

Devices collapse

Certificates collapse

Provisioning Profiles collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeClient

Returns a new instance of Client.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/spaceship/client.rb', line 52

def initialize
  @client = Faraday.new("https://developer.apple.com/services-account/#{PROTOCOL_VERSION}/") do |c|
    c.response :json, content_type: /\bjson$/
    c.response :xml, content_type: /\bxml$/
    c.response :plist, content_type: /\bplist$/
    c.adapter Faraday.default_adapter

    if ENV['DEBUG']
      # for debugging only
      # This enables tracking of networking requests using Charles Web Proxy
      c.response :logger
      c.proxy "https://127.0.0.1:8888"
    end
  end
end

Instance Attribute Details

#clientObject (readonly)

Returns the value of attribute client.



18
19
20
# File 'lib/spaceship/client.rb', line 18

def client
  @client
end

Returns the value of attribute cookie.



19
20
21
# File 'lib/spaceship/client.rb', line 19

def cookie
  @cookie
end

#loggerObject

The logger in which all requests are logged /tmp/spaceship.log by default



23
24
25
# File 'lib/spaceship/client.rb', line 23

def logger
  @logger
end

Class Method Details

.login(user = nil, password = nil) ⇒ Spaceship::Client

Authenticates with Apple’s web services. This method has to be called once to generate a valid session. The session will automatically be used from then on.

This method will automatically use the username from the Appfile (if available) and fetch the password from the Keychain (if available)

Parameters:

  • user (String) (defaults to: nil)

    (optional): The username (usually the email address)

  • password (String) (defaults to: nil)

    (optional): The password

Returns:

Raises:

  • InvalidUserCredentialsError: raised if authentication failed



43
44
45
46
47
48
49
50
# File 'lib/spaceship/client.rb', line 43

def self.(user = nil, password = nil)
  instance = self.new
  if instance.(user, password)
    instance
  else
    raise InvalidUserCredentialsError.new
  end
end

Instance Method Details

#api_keyObject

Fetches the latest API Key from the Apple Dev Portal



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/spaceship/client.rb', line 69

def api_key
  cache_path = "/tmp/spaceship_api_key.txt"
  cached = File.read(cache_path) rescue nil
  return cached if cached

  landing_url = "https://developer.apple.com/membercenter/index.action"
  logger.info("GET: " + landing_url)
  headers = @client.get(landing_url).headers
  results = headers['location'].match(/.*appIdKey=(\h+)/)
  if results.length > 1
    api_key = results[1]
    File.write(cache_path, api_key)
    return api_key
  else
    raise "Could not find latest API Key from the Dev Portal"
  end
end

#appsObject



227
228
229
230
231
232
233
234
235
236
237
# File 'lib/spaceship/client.rb', line 227

def apps
  paging do |page_number|
    r = request(:post, 'account/ios/identifiers/listAppIds.action', {
      teamId: team_id,
      pageNumber: page_number,
      pageSize: page_size,
      sort: 'name=asc'
    })
    parse_response(r, 'appIds')
  end
end

#certificates(types) ⇒ Object



310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/spaceship/client.rb', line 310

def certificates(types)
  paging do |page_number|
    r = request(:post, 'account/ios/certificate/listCertRequests.action', {
      teamId: team_id,
      types: types.join(','),
      pageNumber: page_number,
      pageSize: page_size,
      sort: 'certRequestStatusCode=asc'
    })
    parse_response(r, 'certRequests')
  end
end

#create_app!(type, name, bundle_id) ⇒ Object



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/spaceship/client.rb', line 239

def create_app!(type, name, bundle_id)
  ident_params = case type.to_sym
  when :explicit
    {
      type: 'explicit',
      explicitIdentifier: bundle_id,
      appIdentifierString: bundle_id,
      push: 'on',
      inAppPurchase: 'on',
      gameCenter: 'on'
    }
  when :wildcard
    {
      type: 'wildcard',
      wildcardIdentifier: bundle_id,
      appIdentifierString: bundle_id
    }
  end

  params = {
    appIdName: name,
    teamId: team_id
  }

  params.merge!(ident_params)

  r = request(:post, 'account/ios/identifiers/addAppId.action', params)
  parse_response(r, 'appId')
end

#create_certificate!(type, csr, app_id = nil) ⇒ Object



323
324
325
326
327
328
329
330
331
# File 'lib/spaceship/client.rb', line 323

def create_certificate!(type, csr, app_id = nil)
  r = request(:post, 'account/ios/certificate/submitCertificateRequest.action', {
    teamId: team_id,
    type: type,
    csrContent: csr,
    appIdId: app_id  #optional
  })
  parse_response(r, 'certRequest')
end

#create_device!(device_name, device_id) ⇒ Object



293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/spaceship/client.rb', line 293

def create_device!(device_name, device_id)
  r = request(:post) do |r|
    r.url "https://developerservices2.apple.com/services/#{PROTOCOL_VERSION}/ios/addDevice.action"
    r.params = {
      teamId: team_id,
      deviceNumber: device_id,
      name: device_name
    }
  end

  parse_response(r, 'device')
end

#create_provisioning_profile!(name, distribution_method, app_id, certificate_ids, device_ids) ⇒ Object



369
370
371
372
373
374
375
376
377
378
379
# File 'lib/spaceship/client.rb', line 369

def create_provisioning_profile!(name, distribution_method, app_id, certificate_ids, device_ids)
  r = request(:post, 'account/ios/profile/createProvisioningProfile.action', {
    teamId: team_id,
    provisioningProfileName: name,
    appIdId: app_id,
    distributionType: distribution_method,
    certificateIds: certificate_ids,
    deviceIds: device_ids
  })
  parse_response(r, 'provisioningProfile')
end

#delete_app!(app_id) ⇒ Object



269
270
271
272
273
274
275
# File 'lib/spaceship/client.rb', line 269

def delete_app!(app_id)
  r = request(:post, 'account/ios/identifiers/deleteAppId.action', {
    teamId: team_id,
    appIdId: app_id
  })
  parse_response(r)
end

#delete_provisioning_profile!(profile_id) ⇒ Object



389
390
391
392
393
394
395
# File 'lib/spaceship/client.rb', line 389

def delete_provisioning_profile!(profile_id)
  r = request(:post, 'account/ios/profile/deleteProvisioningProfile.action', {
    teamId: team_id,
    provisioningProfileId: profile_id
  })
  parse_response(r)
end

#devicesObject



281
282
283
284
285
286
287
288
289
290
291
# File 'lib/spaceship/client.rb', line 281

def devices
  paging do |page_number|
    r = request(:post, 'account/ios/device/listDevices.action', {
      teamId: team_id,
      pageNumber: page_number,
      pageSize: page_size,
      sort: 'name=asc'
    })
    parse_response(r, 'devices')
  end
end

#download_certificate(certificate_id, type) ⇒ Object



333
334
335
336
337
338
339
340
341
# File 'lib/spaceship/client.rb', line 333

def download_certificate(certificate_id, type)
  {type: type, certificate_id: certificate_id}.each { |k, v| raise "#{k} must not be nil" if v.nil? }

  r = request(:post, 'https://developer.apple.com/account/ios/certificate/certificateContentDownload.action', {
    displayId: certificate_id,
    type: type
  })
  parse_response(r)
end

#download_provisioning_profile(profile_id) ⇒ Object



381
382
383
384
385
386
387
# File 'lib/spaceship/client.rb', line 381

def download_provisioning_profile(profile_id)
  r = request(:get, 'https://developer.apple.com/account/ios/profile/profileContentDownload.action', {
    teamId: team_id,
    displayId: profile_id
  })
  parse_response(r)
end

#in_house?Boolean

Is the current session from an Enterprise In House account?

Returns:

  • (Boolean)


218
219
220
221
# File 'lib/spaceship/client.rb', line 218

def in_house?
  return @in_house unless @in_house.nil?
  @in_house = (team_information['type'] == 'In-House')
end

#login(user = nil, password = nil) ⇒ Spaceship::Client

Authenticates with Apple’s web services. This method has to be called once to generate a valid session. The session will automatically be used from then on.

This method will automatically use the username from the Appfile (if available) and fetch the password from the Keychain (if available)

Parameters:

  • user (String) (defaults to: nil)

    (optional): The username (usually the email address)

  • password (String) (defaults to: nil)

    (optional): The password

Returns:

Raises:

  • InvalidUserCredentialsError: raised if authentication failed



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/spaceship/client.rb', line 151

def (user = nil, password = nil)
  if user.to_s.empty? or password.to_s.empty?
    require 'credentials_manager'
    data = CredentialsManager::PasswordManager.shared_manager(user, false)
    user ||= data.username
    password ||= data.password
  end

  if user.to_s.empty? or password.to_s.empty?
    raise InvalidUserCredentialsError.new("No login data provided")
  end

  response = request(:post, "https://idmsa.apple.com/IDMSWebAuth/authenticate", {
    appleId: user,
    accountPassword: password,
    appIdKey: api_key
  })

  if response['Set-Cookie'] =~ /myacinfo=(\w+);/
    @cookie = "myacinfo=#{$1};"
    return @client
  else
    # User Credentials are wrong
    raise InvalidUserCredentialsError.new(response)
  end
end

#page_sizeObject

The page size we want to request, defaults to 500



113
114
115
# File 'lib/spaceship/client.rb', line 113

def page_size
  @page_size ||= 500
end

#pagingObject

Handles the paging for you… for free Just pass a block and use the parameter as page number



119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/spaceship/client.rb', line 119

def paging
  page = 0
  results = []
  loop do
    page += 1
    current = yield(page)

    results = results + current

    break if ((current || []).count < page_size) # no more results
  end

  return results
end

#provisioning_profilesObject



356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/spaceship/client.rb', line 356

def provisioning_profiles
  r = request(:post) do |r|
    r.url "https://developerservices2.apple.com/services/#{PROTOCOL_VERSION}/ios/listProvisioningProfiles.action"
    r.params = {
      teamId: team_id,
      includeInactiveProfiles: true,
      onlyCountLists: true,
    }
  end

  parse_response(r, 'provisioningProfiles')
end

#repair_provisioning_profile!(profile_id, name, distribution_method, app_id, certificate_ids, device_ids) ⇒ Object



397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/spaceship/client.rb', line 397

def repair_provisioning_profile!(profile_id, name, distribution_method, app_id, certificate_ids, device_ids)
  r = request(:post, 'account/ios/profile/regenProvisioningProfile.action', {
    teamId: team_id,
    provisioningProfileId: profile_id,
    provisioningProfileName: name,
    appIdId: app_id,
    distributionType: distribution_method,
    certificateIds: certificate_ids.first, # we are most of the times only allowed to pass one
    deviceIds: device_ids
  })

  parse_response(r, 'provisioningProfile')
end

#revoke_certificate!(certificate_id, type) ⇒ Object



343
344
345
346
347
348
349
350
# File 'lib/spaceship/client.rb', line 343

def revoke_certificate!(certificate_id, type)
  r = request(:post, 'account/ios/certificate/revokeCertificate.action', {
    teamId: team_id,
    certificateId: certificate_id,
    type: type
  })
  parse_response(r, 'certRequests')
end

#select_teamObject

Shows a team selection for the user in the terminal. This should not be called on CI systems



201
202
203
# File 'lib/spaceship/client.rb', line 201

def select_team
  @current_team_id = self.UI.select_team
end

#session?Bool

Returns Do we have a valid session?.

Returns:

  • (Bool)

    Do we have a valid session?



179
180
181
# File 'lib/spaceship/client.rb', line 179

def session?
  !!@cookie
end

#team_idString

Returns The currently selected Team ID.

Returns:

  • (String)

    The currently selected Team ID



190
191
192
193
194
195
196
197
# File 'lib/spaceship/client.rb', line 190

def team_id
  return @current_team_id if @current_team_id

  if teams.count > 1
    puts "The current user is in #{teams.count} teams. Pass a team ID or call `select_team` to choose a team. Using the first one for now."
  end
  @current_team_id ||= teams[0]['teamId']
end

#team_id=(team_id) ⇒ Object

Set a new team ID which will be used from now on



206
207
208
# File 'lib/spaceship/client.rb', line 206

def team_id=(team_id)
  @current_team_id = team_id
end

#team_informationHash

Returns Fetches all information of the currently used team.

Returns:

  • (Hash)

    Fetches all information of the currently used team



211
212
213
214
215
# File 'lib/spaceship/client.rb', line 211

def team_information
  teams.find do |t|
    t['teamId'] == team_id
  end
end

#teamsArray

Returns A list of all available teams.

Returns:

  • (Array)

    A list of all available teams



184
185
186
187
# File 'lib/spaceship/client.rb', line 184

def teams
  r = request(:post, 'account/listTeams.action')
  parse_response(r, 'teams')
end

#UIObject

Public getter for all UI related code



11
12
13
# File 'lib/spaceship/ui.rb', line 11

def UI
  UserInterface.new(self)
end