spaceship exposes both the Apple Developer Center and the App Store Connect API. It’s super fast, well tested and supports all of the operations you can do via the browser. It powers parts of fastlane, and can be leveraged for more advanced fastlane features. Scripting your Developer Center workflow has never been easierjavascript
- Blazing fast communication using only a HTTP client
- Object oriented access to all resources
- Resistant against front-end design changes of the of the Apple Developer Portal
- One central tool for the communication
- Automatic re-trying of requests in case a timeout occurs
- No web scraping
- 90%+ test coverage by stubbing server responses
spaceship同時暴露了Apple Developer Center 和 App Store Connect 的api操做,速度快,在瀏覽器上能夠完成的操做,它均可以使用腳本實現。由於spaceship的全部操做都是使用http直接請求蘋果底層api,而非採用網絡爬蟲的方式,因此會比在瀏覽器上操做更快。減小了一些冗餘的網絡資源請求以及加載java
在 Apple Developer Center 下能夠添加證書、設備等操做。在 App Store Connect 中能夠建立新app、修改現有app信息、管理成員信息、提交testflight、獲取財務帳單等等操做。git
因此平時一些頻繁或者複雜的操做徹底可使用spaceship來替代,避免由於使用瀏覽器而進行的漫長等待。並且官方也在一步步的公開api訪問的方式,說明這是一個趨勢github
源碼目錄以下:web
connect_apijson
包裝蘋果官方開放的App Store Connsect APIapi
portal瀏覽器
處理蘋果開發者中心的一些操做,操做證書、設備、描述文件等須要用此套apiruby
test_flightmarkdown
操做app的testflight
tunes
處理appstoreconnect的一些操做,此代碼爲老版的操做方式,包含api比較全。部分操做可以使用 connect_api 採用蘋果官方開發的api
spaceship的全部操做都須要登陸,因此首先研究的就是登陸的步驟。
require 'spaceship'
# tunes登陸
tunes = Spaceship::Tunes.login('xxx', 'xxx')
# portal登陸
portal = Spaceship::Portal.login('xxx', 'xxx');
複製代碼
調用鏈以下:
client.rb
中的 login()
方法檢驗帳號密碼的合法性,而後調用client.rb
中的 do_login()
方法,內部調用子類中實現的 `send_login_request()方法
send_login_request
的具體實如今對應的子類中(tunes_client.rb
,portal_client.rb
)
def send_login_request(user, password)
clear_user_cached_data
result = send_shared_login_request(user, password)
store_cookie
return result
end
複製代碼
最終的邏輯處理都是在client.rb
中的 send_shared_login_request()
方法內
def send_shared_login_request(user, password)
if load_session_from_file
# Check if the session is still valid here
begin
return true if fetch_olympus_session
rescue
puts("Available session is not valid any more. Continuing with normal login.")
end
end
# The user can pass the session via environment variable (Mainly used in CI environments)
if load_session_from_env
# see above
begin
# see above
return true if fetch_olympus_session
rescue
puts("Session loaded from environment variable is not valid. Continuing with normal login.")
# see above
end
end
#
# After this point, we sure have no valid session any more and have to create a new one
#
data = {
accountName: user,
password: password,
rememberMe: true
}
begin
# 請求登陸接口
# Now we know if the login is successful or if we need to do 2 factor
# 根據返回狀態執行下一步操做
case response.status
when 403
raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
when 200
fetch_olympus_session
return response
when 409
# 2 step/factor is enabled for this account, first handle that
handle_two_step_or_factor(response)
# and then get the olympus session
fetch_olympus_session
return true
else
......
end
end
複製代碼
從 load_session_from_file
中加載cookie數據,若是有數據,調用fetch_olympus_session
判斷session是否有效,如有效,說明登陸還在有效期,則直接返回成功
從 load_session_from_env
中獲取用戶是否設置了本地環境變量(ENV["FASTLANE_SESSION"] || ENV["SPACESHIP_SESSION"]
)存儲session值,如有值,接下來跟第一步後續操做同樣,判斷session是否有效
若前兩步都未成功,咱們須要從新登陸,獲取有效的session
data = {
accountName: user,
password: password,
rememberMe: true
}
important_cookie = @cookie.store.entries.find { |a| a.name.include?("DES") }
if important_cookie
modified_cookie = self.cookie # returns a string of all cookies
unescaped_important_cookie = "#{important_cookie.name}=#{important_cookie.value}"
escaped_important_cookie = "#{important_cookie.name}=\"#{important_cookie.value}\""
modified_cookie.gsub!(unescaped_important_cookie, escaped_important_cookie)
end
response = request(:post) do |req|
req.url("https://idmsa.apple.com/appleauth/auth/signin")
req.body = data.to_json
req.headers['Content-Type'] = 'application/json'
req.headers['X-Requested-With'] = 'XMLHttpRequest'
req.headers['X-Apple-Widget-Key'] = self.itc_service_key
req.headers['Accept'] = 'application/json, text/javascript'
req.headers["Cookie"] = modified_cookie if modified_cookie
end
複製代碼
調用登陸接口完成以後,根據response.status判斷執行下一步操做
403
用戶或密碼有問題,登陸不成功
200
登陸成功。調用 fetch_olympus_session
請求session數據
409
首先須要進行雙重認證 handle_two_step_or_factor(response)
,而後請求session數據。
雙重認證的處理邏輯都在此 two_step_or_factor_client.rb
文件中處理,包含了受信任設備以及受信任手機號的處理,完成驗證以後會存儲session,下一次請求直接加載seesion,無需再次登陸
if r.body.kind_of?(Hash) && r.body["trustedDevices"].kind_of?(Array)
# 受信任設備
handle_two_step(r)
elsif r.body.kind_of?(Hash) && r.body["trustedPhoneNumbers"].kind_of?(Array) && r.body["trustedPhoneNumbers"].first.kind_of?(Hash)
# 受信任手機號
handle_two_factor(r)
else
...
end
複製代碼
# Responsible for setting all required header attributes for the requests
# to succeed
def update_request_headers(req)
req.headers["X-Apple-Id-Session-Id"] = @x_apple_id_session_id
req.headers["X-Apple-Widget-Key"] = self.itc_service_key
req.headers["Accept"] = "application/json"
req.headers["scnt"] = @scnt
end
複製代碼
可實現的功能詳細參考這裏 或者直接查看源碼 tunes_client.rb
拿兩個簡單的例子來看一下具體的用法。這裏使用的是老版本的方式,並非蘋果開放的官方apiApp Store Connect API
require 'spaceship'
# 1.登陸
Spaceship::Tunes.login(user, pwd)
Spaceship::Tunes.select_team(team_id: xxx)
# 2.根據bundleId選擇app
app = Spaceship::Tunes::Application.find(bundle_id)
# 3.調用解決方案中心接口
response = app.resolution_center
# 4.根據response查找出最新信息並郵件通知對應負責人
...
複製代碼
上圖爲解決方案中心實際返回的json數據,能夠經過抓包查看數據結構,也能夠直接調用spaceship的接口查看返回值。本身根據狀況對數據進行自定義的分析處理。
ps:原本準備本身經過抓包找到接口調用,後來在寫的過程當中發現spaceship已經給提供了對應的方法resolution_center
以及解釋。因此之後再遇到一些想實現的功能時,能夠提早查看源碼看系統是否已經提供,避免走彎路
# @return (Hash) Contains the reason for rejection.
# if everything is alright, the result will be
# `{"sectionErrorKeys"=>[], "sectionInfoKeys"=>[], "sectionWarningKeys"=>[], "replyConstraints"=>{"minLength"=>1, "maxLength"=>4000}, "appNotes"=>{"threads"=>[]}, "betaNotes"=>{"threads"=>[]}, "appMessages"=>{"threads"=>[]}}`
def resolution_center
client.get_resolution_center(apple_id, platform)
end
複製代碼
require 'spaceship'
# 1.登陸
Spaceship::Tunes.login(user, pwd)
Spaceship::Tunes.select_team(team_id: xxx)
# 2.根據bundleId選擇app
app = Spaceship::Tunes::Application.find(bundle_id)
# 3. 建立內購商品
begin
@app.in_app_purchases.create!( # 建立商品
type: iapType,
versions: {
"zh-Hans": {
name: name,
description: desc
}
},
reference_name: reference,
product_id: product_id,
review_notes: review_desc,
review_screenshot: review_image_path,
pricing_intervals: [
{
country: "WW",
begin_date: nil,
end_date: nil,
tier: tier
}
],
family_id: family_id,
subscription_free_trial: subscription_free_trial,
subscription_duration: subscription_duration,
subscription_price_target: subscription_price_target
)
puts "#建立成功!!! "
rescue Exception => error
puts "#建立失敗: #{error} "
end
複製代碼
iapType商品對應的類型
# 獲取到對應的商品類型
def get_correct_iapType(type)
if type == "消耗品"
return Spaceship::Tunes::IAPType::CONSUMABLE
elsif type == "非消耗品"
return Spaceship::Tunes::IAPType::NON_CONSUMABLE
elsif type == "非自動續訂"
return Spaceship::Tunes::IAPType::NON_RENEW_SUBSCRIPTION
elsif type == "自動續訂"
return Spaceship::Tunes::IAPType::RECURRING
end
end
複製代碼
product_id
商品惟一id
...
對應的還有對商品修改方法
# 根據商品id查找到對應商品
purch = @app.in_app_purchases.find(product_id)
e = purch.edit # 編輯商品
e.versions = {
"zh-Hans": {
name: name,
description: desc
}
}
e.reference_name = reference
e.review_notes = review_desc
e.review_screenshot = review_image_path
e.pricing_intervals = [
{
country: "WW",
begin_date: nil,
end_date: nil,
tier: tier
}
]
# family_id = family_id,
e.subscription_free_trial = subscription_free_trial,
e.subscription_duration = subscription_duration,
e.subscription_price_target = subscription_price_target
e.save!
複製代碼
建立羣組方法
value = create_family(
name: product["family_name"],
product_id: product_id,
reference_name: name,
versions: {
"zh-Hans": {
subscription_name: "xxx",
name: product["family_name"]
}
}
)
複製代碼
可實現的功能詳細參考這裏 或者直接查看源碼 portal_client.rb